Merge branch 'master' of github.com:spotify/backstage into ben/listing-components
* 'master' of github.com:spotify/backstage: frontend/core: restructure api a bit frontend/core: added plugin route registration api + moved home page to plugin Update yarn.lock Fix proto/README.md typo remove createGlobalStyles from theme Add iOS Swift template frontend/core: add some kinda app api remove tinycolor. remove muithemeprovider fix tests Remove hello proto more deps bring in Page, Header, InfoCard, ErrorBoundary, BackstageTheme, Navigation, SupportButtons, HeaderAction, HeaderLabel components from backstage frontend
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"component_id": "",
|
||||
"owner": "",
|
||||
"description": "We promise to update this description /{{cookiecutter.owner}}"
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schema": [
|
||||
{
|
||||
"id": "component_id",
|
||||
"title": "Project name",
|
||||
"type": "text",
|
||||
"description": "Project name used to generate Xcode files and class names. CamelCase e.g. 'MyProject'",
|
||||
"validators": [
|
||||
{
|
||||
"type": "required"
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"match": "^[A-Z][A-Za-z]+$",
|
||||
"message": "Project name must be CamelCase"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "owner",
|
||||
"title": "owner",
|
||||
"type": "text",
|
||||
"description": "The owner name used to identify the owner of this component",
|
||||
"validators": [
|
||||
{
|
||||
"type": "required"
|
||||
},
|
||||
{
|
||||
"type": "string-range",
|
||||
"min": 4,
|
||||
"max": 33
|
||||
},
|
||||
{
|
||||
"type": "regex",
|
||||
"match": "^[a-z][a-z0-9]+$",
|
||||
"message": "Owner names must consist only of lowercase letters"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# iOS App
|
||||
|
||||
This project provides a template project to generate an iOS App written in Swift.
|
||||
|
||||
## Building and Testing
|
||||
|
||||
You can directly open the `xcodeproj` and build from Xcode or run `build.sh` from the command line.
|
||||
Executable
+21
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
PLATFORM="platform=iOS Simulator,OS=13.0,name=iPhone 11"
|
||||
SDK="iphonesimulator"
|
||||
|
||||
set -e
|
||||
function trap_handler() {
|
||||
echo -e "\n\nFailed to build project"
|
||||
exit 255
|
||||
}
|
||||
trap trap_handler INT TERM EXIT
|
||||
|
||||
echo "Building {{cookiecutter.component_id}}."
|
||||
xcodebuild \
|
||||
-project {{cookiecutter.component_id}}.xcodeproj \
|
||||
-scheme {{cookiecutter.component_id}} \
|
||||
-sdk "$SDK" \
|
||||
-destination "$PLATFORM" \
|
||||
build
|
||||
trap - EXIT
|
||||
exit 0
|
||||
+470
@@ -0,0 +1,470 @@
|
||||
// !$*UTF8*$!
|
||||
{
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
C5B1973423EAFA6D007C9A28 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1973323EAFA6D007C9A28 /* AppDelegate.swift */; };
|
||||
C5B1973623EAFA6D007C9A28 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1973523EAFA6D007C9A28 /* SceneDelegate.swift */; };
|
||||
C5B1973823EAFA6D007C9A28 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1973723EAFA6D007C9A28 /* ViewController.swift */; };
|
||||
C5B1973B23EAFA6D007C9A28 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5B1973923EAFA6D007C9A28 /* Main.storyboard */; };
|
||||
C5B1973D23EAFA6E007C9A28 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = C5B1973C23EAFA6E007C9A28 /* Assets.xcassets */; };
|
||||
C5B1974023EAFA6E007C9A28 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C5B1973E23EAFA6E007C9A28 /* LaunchScreen.storyboard */; };
|
||||
C5B1974B23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B1974A23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
C5B1974723EAFA6E007C9A28 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = C5B1972823EAFA6D007C9A28 /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = C5B1972F23EAFA6D007C9A28;
|
||||
remoteInfo = {{cookiecutter.component_id}};
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
C5B1973023EAFA6D007C9A28 /* {{cookiecutter.component_id}}.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = {{cookiecutter.component_id}}.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C5B1973323EAFA6D007C9A28 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
C5B1973523EAFA6D007C9A28 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
|
||||
C5B1973723EAFA6D007C9A28 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = "<group>"; };
|
||||
C5B1973A23EAFA6D007C9A28 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||
C5B1973C23EAFA6E007C9A28 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
C5B1973F23EAFA6E007C9A28 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
C5B1974123EAFA6E007C9A28 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
C5B1974623EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = {{cookiecutter.component_id}}Tests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
C5B1974A23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = {{cookiecutter.component_id}}Tests.swift; sourceTree = "<group>"; };
|
||||
C5B1974C23EAFA6E007C9A28 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
C5B1972D23EAFA6D007C9A28 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C5B1974323EAFA6E007C9A28 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
C5B1972723EAFA6D007C9A28 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C5B1973223EAFA6D007C9A28 /* {{cookiecutter.component_id}} */,
|
||||
C5B1974923EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */,
|
||||
C5B1973123EAFA6D007C9A28 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C5B1973123EAFA6D007C9A28 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C5B1973023EAFA6D007C9A28 /* {{cookiecutter.component_id}}.app */,
|
||||
C5B1974623EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C5B1973223EAFA6D007C9A28 /* {{cookiecutter.component_id}} */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C5B1973323EAFA6D007C9A28 /* AppDelegate.swift */,
|
||||
C5B1973523EAFA6D007C9A28 /* SceneDelegate.swift */,
|
||||
C5B1973723EAFA6D007C9A28 /* ViewController.swift */,
|
||||
C5B1973923EAFA6D007C9A28 /* Main.storyboard */,
|
||||
C5B1973C23EAFA6E007C9A28 /* Assets.xcassets */,
|
||||
C5B1973E23EAFA6E007C9A28 /* LaunchScreen.storyboard */,
|
||||
C5B1974123EAFA6E007C9A28 /* Info.plist */,
|
||||
);
|
||||
path = {{cookiecutter.component_id}};
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C5B1974923EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C5B1974A23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift */,
|
||||
C5B1974C23EAFA6E007C9A28 /* Info.plist */,
|
||||
);
|
||||
path = {{cookiecutter.component_id}}Tests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
C5B1972F23EAFA6D007C9A28 /* {{cookiecutter.component_id}} */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C5B1974F23EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}" */;
|
||||
buildPhases = (
|
||||
C5B1972C23EAFA6D007C9A28 /* Sources */,
|
||||
C5B1972D23EAFA6D007C9A28 /* Frameworks */,
|
||||
C5B1972E23EAFA6D007C9A28 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = {{cookiecutter.component_id}};
|
||||
productName = {{cookiecutter.component_id}};
|
||||
productReference = C5B1973023EAFA6D007C9A28 /* {{cookiecutter.component_id}}.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
C5B1974523EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = C5B1975223EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}Tests" */;
|
||||
buildPhases = (
|
||||
C5B1974223EAFA6E007C9A28 /* Sources */,
|
||||
C5B1974323EAFA6E007C9A28 /* Frameworks */,
|
||||
C5B1974423EAFA6E007C9A28 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
C5B1974823EAFA6E007C9A28 /* PBXTargetDependency */,
|
||||
);
|
||||
name = {{cookiecutter.component_id}}Tests;
|
||||
productName = {{cookiecutter.component_id}}Tests;
|
||||
productReference = C5B1974623EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
C5B1972823EAFA6D007C9A28 /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastSwiftUpdateCheck = 1100;
|
||||
LastUpgradeCheck = 1100;
|
||||
ORGANIZATIONNAME = Organization;
|
||||
TargetAttributes = {
|
||||
C5B1972F23EAFA6D007C9A28 = {
|
||||
CreatedOnToolsVersion = 11.0;
|
||||
};
|
||||
C5B1974523EAFA6E007C9A28 = {
|
||||
CreatedOnToolsVersion = 11.0;
|
||||
TestTargetID = C5B1972F23EAFA6D007C9A28;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = C5B1972B23EAFA6D007C9A28 /* Build configuration list for PBXProject "{{cookiecutter.component_id}}" */;
|
||||
compatibilityVersion = "Xcode 9.3";
|
||||
developmentRegion = en;
|
||||
hasScannedForEncodings = 0;
|
||||
knownRegions = (
|
||||
en,
|
||||
Base,
|
||||
);
|
||||
mainGroup = C5B1972723EAFA6D007C9A28;
|
||||
productRefGroup = C5B1973123EAFA6D007C9A28 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
C5B1972F23EAFA6D007C9A28 /* {{cookiecutter.component_id}} */,
|
||||
C5B1974523EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
C5B1972E23EAFA6D007C9A28 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C5B1974023EAFA6E007C9A28 /* LaunchScreen.storyboard in Resources */,
|
||||
C5B1973D23EAFA6E007C9A28 /* Assets.xcassets in Resources */,
|
||||
C5B1973B23EAFA6D007C9A28 /* Main.storyboard in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C5B1974423EAFA6E007C9A28 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
C5B1972C23EAFA6D007C9A28 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C5B1973823EAFA6D007C9A28 /* ViewController.swift in Sources */,
|
||||
C5B1973423EAFA6D007C9A28 /* AppDelegate.swift in Sources */,
|
||||
C5B1973623EAFA6D007C9A28 /* SceneDelegate.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
C5B1974223EAFA6E007C9A28 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C5B1974B23EAFA6E007C9A28 /* {{cookiecutter.component_id}}Tests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
C5B1974823EAFA6E007C9A28 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = C5B1972F23EAFA6D007C9A28 /* {{cookiecutter.component_id}} */;
|
||||
targetProxy = C5B1974723EAFA6E007C9A28 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
C5B1973923EAFA6D007C9A28 /* Main.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
C5B1973A23EAFA6D007C9A28 /* Base */,
|
||||
);
|
||||
name = Main.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C5B1973E23EAFA6E007C9A28 /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
C5B1973F23EAFA6E007C9A28 /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
C5B1974D23EAFA6E007C9A28 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_DYNAMIC_NO_PIC = NO;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_OPTIMIZATION_LEVEL = 0;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C5B1974E23EAFA6E007C9A28 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||
CLANG_ANALYZER_NONNULL = YES;
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||
CLANG_CXX_LIBRARY = "libc++";
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CLANG_ENABLE_OBJC_ARC = YES;
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||
CLANG_WARN_COMMA = YES;
|
||||
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_EMPTY_BODY = YES;
|
||||
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||
CLANG_WARN_INT_CONVERSION = YES;
|
||||
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
GCC_NO_COMMON_BLOCKS = YES;
|
||||
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
MTL_FAST_MATH = YES;
|
||||
SDKROOT = iphoneos;
|
||||
SWIFT_COMPILATION_MODE = wholemodule;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||
VALIDATE_PRODUCT = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C5B1975023EAFA6E007C9A28 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = {{cookiecutter.component_id}}/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}};
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C5B1975123EAFA6E007C9A28 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = {{cookiecutter.component_id}}/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}};
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
C5B1975323EAFA6E007C9A28 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = {{cookiecutter.component_id}}Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}}Tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/{{cookiecutter.component_id}}.app/{{cookiecutter.component_id}}";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
C5B1975423EAFA6E007C9A28 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
INFOPLIST_FILE = {{cookiecutter.component_id}}Tests/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@loader_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.organization.{{cookiecutter.component_id}}Tests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/{{cookiecutter.component_id}}.app/{{cookiecutter.component_id}}";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
/* End XCBuildConfiguration section */
|
||||
|
||||
/* Begin XCConfigurationList section */
|
||||
C5B1972B23EAFA6D007C9A28 /* Build configuration list for PBXProject "{{cookiecutter.component_id}}" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C5B1974D23EAFA6E007C9A28 /* Debug */,
|
||||
C5B1974E23EAFA6E007C9A28 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C5B1974F23EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C5B1975023EAFA6E007C9A28 /* Debug */,
|
||||
C5B1975123EAFA6E007C9A28 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
C5B1975223EAFA6E007C9A28 /* Build configuration list for PBXNativeTarget "{{cookiecutter.component_id}}Tests" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
C5B1975323EAFA6E007C9A28 /* Debug */,
|
||||
C5B1975423EAFA6E007C9A28 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = C5B1972823EAFA6D007C9A28 /* Project object */;
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:{{cookiecutter.component_id}}.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
Binary file not shown.
+14
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>SchemeUserState</key>
|
||||
<dict>
|
||||
<key>{{cookiecutter.component_id}}.xcscheme_^#shared#^_</key>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// AppDelegate.swift
|
||||
// {{cookiecutter.component_id}}
|
||||
//
|
||||
// Copyright © 2020 Organization. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
// Override point for customization after application launch.
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UISceneSession Lifecycle
|
||||
|
||||
func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
|
||||
// Called when the user discards a scene session.
|
||||
// If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions.
|
||||
// Use this method to release any resources that were specific to the discarded scenes, as they will not return.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "20x20",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "29x29",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "40x40",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"size" : "60x60",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "20x20",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "29x29",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "40x40",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "76x76",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"size" : "83.5x83.5",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"size" : "1024x1024",
|
||||
"scale" : "1x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
|
||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="ViewController" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
|
||||
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
+64
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
<false/>
|
||||
<key>UISceneConfigurations</key>
|
||||
<dict>
|
||||
<key>UIWindowSceneSessionRoleApplication</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>UISceneConfigurationName</key>
|
||||
<string>Default Configuration</string>
|
||||
<key>UISceneDelegateClassName</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
|
||||
<key>UISceneStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
</dict>
|
||||
</array>
|
||||
</dict>
|
||||
</dict>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array>
|
||||
<string>armv7</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
//
|
||||
// SceneDelegate.swift
|
||||
// {{cookiecutter.component_id}}
|
||||
//
|
||||
// Copyright © 2020 Organization. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
var window: UIWindow?
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
|
||||
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
|
||||
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
|
||||
guard let _ = (scene as? UIWindowScene) else { return }
|
||||
}
|
||||
|
||||
func sceneDidDisconnect(_ scene: UIScene) {
|
||||
// Called as the scene is being released by the system.
|
||||
// This occurs shortly after the scene enters the background, or when its session is discarded.
|
||||
// Release any resources associated with this scene that can be re-created the next time the scene connects.
|
||||
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
|
||||
}
|
||||
|
||||
func sceneDidBecomeActive(_ scene: UIScene) {
|
||||
// Called when the scene has moved from an inactive state to an active state.
|
||||
// Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive.
|
||||
}
|
||||
|
||||
func sceneWillResignActive(_ scene: UIScene) {
|
||||
// Called when the scene will move from an active state to an inactive state.
|
||||
// This may occur due to temporary interruptions (ex. an incoming phone call).
|
||||
}
|
||||
|
||||
func sceneWillEnterForeground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the background to the foreground.
|
||||
// Use this method to undo the changes made on entering the background.
|
||||
}
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
// Called as the scene transitions from the foreground to the background.
|
||||
// Use this method to save data, release shared resources, and store enough scene-specific state information
|
||||
// to restore the scene back to its current state.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// ViewController.swift
|
||||
// {{cookiecutter.component_id}}
|
||||
//
|
||||
// Copyright © 2020 Organization. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
|
||||
class ViewController: UIViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
// Do any additional setup after loading the view.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
</dict>
|
||||
</plist>
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// {{cookiecutter.component_id}}Tests.swift
|
||||
// {{cookiecutter.component_id}}Tests
|
||||
//
|
||||
// Copyright © 2020 Organization. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import {{cookiecutter.component_id}}
|
||||
|
||||
class {{cookiecutter.component_id}}Tests: XCTestCase {
|
||||
|
||||
override func setUp() {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||
}
|
||||
|
||||
func testExample() {
|
||||
// This is an example of a functional test case.
|
||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||
}
|
||||
|
||||
func testPerformanceExample() {
|
||||
// This is an example of a performance test case.
|
||||
self.measure {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@
|
||||
"dependencies": {
|
||||
"@backstage/core": "0.0.0",
|
||||
"@backstage/plugin-hello-world": "0.0.0",
|
||||
"@backstage/plugin-home-page": "0.0.0",
|
||||
"@backstage/plugin-login": "0.0.0",
|
||||
"@react-workspaces/react-scripts": "^3.3.0-alpha-08",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
|
||||
@@ -6,7 +6,5 @@ describe('App', () => {
|
||||
it('renders learn react link', () => {
|
||||
const rendered = render(<App />);
|
||||
rendered.getByText('This is Backstage!');
|
||||
rendered.getByText('Backstage is an open platform for building developer portals');
|
||||
rendered.getByText('…with plugin hello-world:');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,18 +1,41 @@
|
||||
import React, { FC, Fragment } from 'react';
|
||||
import helloWorld, { MyComponent } from '@backstage/plugin-hello-world';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import SideBar from './components/SideBar';
|
||||
import PageHeader from './components/PageHeader';
|
||||
import {
|
||||
BackstageTheme,
|
||||
createApp,
|
||||
Header,
|
||||
InfoCard,
|
||||
Page,
|
||||
theme,
|
||||
} from '@backstage/core';
|
||||
//import PageHeader from './components/PageHeader';
|
||||
import { LoginComponent } from '@backstage/plugin-login';
|
||||
import HomePagePlugin from '@backstage/plugin-home-page';
|
||||
import { CssBaseline, makeStyles, ThemeProvider } from '@material-ui/core';
|
||||
import React, { FC } from 'react';
|
||||
import {
|
||||
BrowserRouter as Router,
|
||||
Switch,
|
||||
Route,
|
||||
Link as RouterLink,
|
||||
Route,
|
||||
Switch,
|
||||
} from 'react-router-dom';
|
||||
import HomePageTimer from './components/HomepageTimer';
|
||||
import SideBar from './components/SideBar';
|
||||
import entities from './entities';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
'@global': {
|
||||
html: {
|
||||
height: '100%',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
},
|
||||
body: {
|
||||
height: '100%',
|
||||
fontFamily: theme.typography.fontFamily,
|
||||
},
|
||||
a: {
|
||||
color: 'inherit',
|
||||
textDecoration: 'none',
|
||||
},
|
||||
},
|
||||
root: {
|
||||
display: 'grid',
|
||||
// FIXME: Don't used a fixed width here
|
||||
@@ -26,54 +49,21 @@ const useStyles = makeStyles(theme => ({
|
||||
overflowY: 'auto',
|
||||
},
|
||||
pageBody: {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingTop: theme.spacing(2),
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
avatarButton: {
|
||||
padding: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
const App: FC<{}> = () => {
|
||||
return (
|
||||
<AppShell>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route exact path="/">
|
||||
<Home />
|
||||
</Route>
|
||||
<Route path="/login">
|
||||
<Login />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
const Home: FC<{}> = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<Typography variant="body1">
|
||||
{' '}
|
||||
…with plugin {helloWorld?.id ?? 'wat'}:
|
||||
</Typography>
|
||||
<MyComponent />
|
||||
<div>
|
||||
<RouterLink to="/login">Go to Login</RouterLink>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
const Login: FC<{}> = () => {
|
||||
return (
|
||||
<Fragment>
|
||||
<InfoCard title="Login Page">
|
||||
<LoginComponent />
|
||||
<div>
|
||||
<RouterLink to="/">Go to Home</RouterLink>
|
||||
</div>
|
||||
</Fragment>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,12 +73,44 @@ const AppShell: FC<{}> = ({ children }) => {
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<SideBar />
|
||||
<div className={classes.mainContentArea}>
|
||||
<PageHeader />
|
||||
<div className={classes.pageBody}>{children}</div>
|
||||
</div>
|
||||
<Page theme={theme.home}>
|
||||
<div className={classes.mainContentArea}>
|
||||
<Header title="This is Backstage!">
|
||||
<HomePageTimer />
|
||||
</Header>
|
||||
<div className={classes.pageBody}>{children}</div>
|
||||
</div>
|
||||
</Page>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const app = createApp();
|
||||
|
||||
app.registerEntityKind(...entities);
|
||||
app.registerPlugin(HomePagePlugin);
|
||||
|
||||
const AppComponent = app.build();
|
||||
|
||||
const App: FC<{}> = () => {
|
||||
return (
|
||||
<CssBaseline>
|
||||
<ThemeProvider theme={BackstageTheme}>
|
||||
<AppShell>
|
||||
<Router>
|
||||
<Switch>
|
||||
<Route path="/login">
|
||||
<Login />
|
||||
</Route>
|
||||
<Route>
|
||||
<AppComponent />
|
||||
</Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
</AppShell>
|
||||
</ThemeProvider>
|
||||
</CssBaseline>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import React, { FC } from 'react';
|
||||
import { HeaderLabel } from '@backstage/core';
|
||||
|
||||
const timeFormat = { hour: '2-digit', minute: '2-digit' };
|
||||
const nycOptions = { timeZone: 'America/New_York', ...timeFormat };
|
||||
const utcOptions = { timeZone: 'UTC', ...timeFormat };
|
||||
const lonOptions = { timeZone: 'Europe/London', ...timeFormat };
|
||||
const stoOptions = { timeZone: 'Europe/Stockholm', ...timeFormat };
|
||||
|
||||
const defaultTimes = {
|
||||
timeNY: '',
|
||||
timeUTC: '',
|
||||
timeLON: '',
|
||||
timeSTO: '',
|
||||
};
|
||||
|
||||
function getTimes() {
|
||||
const d = new Date();
|
||||
const lang = window.navigator.language;
|
||||
|
||||
// Using the browser native toLocaleTimeString instead of huge moment-tz
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleTimeString
|
||||
const timeNY = d.toLocaleTimeString(lang, nycOptions);
|
||||
const timeUTC = d.toLocaleTimeString(lang, utcOptions);
|
||||
const timeLON = d.toLocaleTimeString(lang, lonOptions);
|
||||
const timeSTO = d.toLocaleTimeString(lang, stoOptions);
|
||||
|
||||
return { timeNY, timeUTC, timeLON, timeSTO };
|
||||
}
|
||||
|
||||
const HomePageTimer: FC<{}> = () => {
|
||||
const [{ timeNY, timeUTC, timeLON, timeSTO }, setTimes] = React.useState(
|
||||
defaultTimes,
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
setTimes(getTimes());
|
||||
|
||||
const intervalId = setInterval(() => {
|
||||
setTimes(getTimes());
|
||||
}, 1000);
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeaderLabel label="NYC" value={timeNY} />
|
||||
<HeaderLabel label="UTC" value={timeUTC} />
|
||||
<HeaderLabel label="LON" value={timeLON} />
|
||||
<HeaderLabel label="STO" value={timeSTO} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePageTimer;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './HomepageTimer';
|
||||
@@ -1,28 +0,0 @@
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import React, { FC } from 'react';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
background: 'linear-gradient(262.63deg, #19D15A 4.2%, #1CAB5B 72.01%)',
|
||||
height: '120px',
|
||||
paddingTop: theme.spacing(2),
|
||||
paddingLeft: theme.spacing(2),
|
||||
color: 'white',
|
||||
},
|
||||
}));
|
||||
|
||||
const SideBar: FC<{}> = () => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<header className={classes.root}>
|
||||
<Typography variant="h2">This is Backstage!</Typography>
|
||||
<Typography variant="caption">
|
||||
Backstage is an open platform for building developer portals
|
||||
</Typography>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default SideBar;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from './PageHeader';
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useEntityUri } from '@backstage/core';
|
||||
|
||||
const MockEntityPage: FC<{}> = () => {
|
||||
const uri = useEntityUri();
|
||||
return <span>Mock card for {uri}, replace with some userful plugin</span>;
|
||||
};
|
||||
|
||||
export default MockEntityPage;
|
||||
@@ -0,0 +1,9 @@
|
||||
import React, { FC } from 'react';
|
||||
import { useEntityUri } from '@backstage/core';
|
||||
|
||||
const MockEntityPage: FC<{}> = () => {
|
||||
const uri = useEntityUri();
|
||||
return <span>Mock page for {uri}, replace with some userful plugin</span>;
|
||||
};
|
||||
|
||||
export default MockEntityPage;
|
||||
@@ -0,0 +1,32 @@
|
||||
import {
|
||||
createEntityKind,
|
||||
createWidgetView,
|
||||
createEntityView,
|
||||
} from '@backstage/core';
|
||||
import ComputerIcon from '@material-ui/icons/Computer';
|
||||
import MockEntityPage from './MockEntityPage';
|
||||
import MockEntityCard from './MockEntityCard';
|
||||
|
||||
/* SERVICE */
|
||||
const serviceOverviewPage = createWidgetView()
|
||||
.addComponent(MockEntityCard)
|
||||
.addComponent(MockEntityCard);
|
||||
|
||||
const serviceView = createEntityView()
|
||||
.addPage('Overview', 'overview', serviceOverviewPage)
|
||||
.addComponent('CI/CD', 'ci-cd', MockEntityPage);
|
||||
|
||||
const serviceEntity = createEntityKind({
|
||||
kind: 'service',
|
||||
title: 'Service',
|
||||
color: {
|
||||
primary: '#f00',
|
||||
secondary: '#ba5',
|
||||
},
|
||||
icon: ComputerIcon,
|
||||
pages: {
|
||||
view: serviceView,
|
||||
},
|
||||
});
|
||||
|
||||
export default [serviceEntity];
|
||||
@@ -12,8 +12,13 @@
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-router-dom": "^5.1.3",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0"
|
||||
"react-dom": "^16.12.0",
|
||||
"react-helmet": "5.2.1",
|
||||
"react-addons-text-content": "0.0.4",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"recompose": "0.30.0"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "web-scripts lint",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import AppBuilder from './app/AppBuilder';
|
||||
import EntityKind, { EntityConfig } from './entity/EntityKind';
|
||||
import WidgetViewBuilder from './widgetView/WidgetViewBuilder';
|
||||
import EntityViewBuilder from './entityView/EntityViewPageBuilder';
|
||||
import BackstagePlugin, { PluginConfig } from './plugin/Plugin';
|
||||
|
||||
export function createApp() {
|
||||
return new AppBuilder();
|
||||
}
|
||||
|
||||
export function createEntityKind(config: EntityConfig) {
|
||||
return new EntityKind(config);
|
||||
}
|
||||
|
||||
export function createWidgetView() {
|
||||
return new WidgetViewBuilder();
|
||||
}
|
||||
|
||||
export function createEntityView() {
|
||||
return new EntityViewBuilder();
|
||||
}
|
||||
|
||||
export function createPlugin(config: PluginConfig): BackstagePlugin {
|
||||
return new BackstagePlugin(config);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import { Route, Switch, useParams } from 'react-router-dom';
|
||||
import { AppContextProvider } from './AppContext';
|
||||
import { App, AppComponentBuilder } from './types';
|
||||
import EntityKind, { EntityConfig } from '../entity/EntityKind';
|
||||
import { EntityContextProvider } from '../entityView/EntityContext';
|
||||
import BackstagePlugin, { registerSymbol } from '../plugin/Plugin';
|
||||
|
||||
class AppImpl implements App {
|
||||
constructor(private readonly entities: Map<string, EntityKind>) {}
|
||||
|
||||
getEntityConfig(kind: string): EntityConfig {
|
||||
const entity = this.entities.get(kind);
|
||||
if (!entity) {
|
||||
throw new Error('EntityKind not found');
|
||||
}
|
||||
return entity.config;
|
||||
}
|
||||
}
|
||||
|
||||
function builtComponent(
|
||||
app: App,
|
||||
component: ComponentType<any> | AppComponentBuilder,
|
||||
) {
|
||||
if (component instanceof AppComponentBuilder) {
|
||||
return component.build(app);
|
||||
}
|
||||
return component;
|
||||
}
|
||||
|
||||
export default class AppBuilder {
|
||||
private readonly entities = new Map<string, EntityKind>();
|
||||
private readonly plugins = new Set<BackstagePlugin>();
|
||||
|
||||
registerEntityKind(...entity: EntityKind[]) {
|
||||
for (const e of entity) {
|
||||
const { kind } = e.config;
|
||||
if (this.entities.has(e.config.kind)) {
|
||||
throw new Error(`EntityKind '${kind}' is already registered`);
|
||||
}
|
||||
this.entities.set(e.config.kind, e);
|
||||
}
|
||||
}
|
||||
|
||||
registerPlugin(...plugin: BackstagePlugin[]) {
|
||||
for (const p of plugin) {
|
||||
if (this.plugins.has(p)) {
|
||||
throw new Error(`Plugin '${p}' is already registered`);
|
||||
}
|
||||
this.plugins.add(p);
|
||||
}
|
||||
}
|
||||
|
||||
build(): ComponentType<{}> {
|
||||
const app = new AppImpl(this.entities);
|
||||
|
||||
const entityRoutes = new Array<JSX.Element>();
|
||||
|
||||
for (const { config } of this.entities.values()) {
|
||||
const { kind, pages } = config;
|
||||
const basePath = `/entity/${kind}`;
|
||||
|
||||
if (pages.list) {
|
||||
const ListComponent = builtComponent(app, pages.list);
|
||||
|
||||
const Component: FC<{}> = () => (
|
||||
<EntityContextProvider config={config}>
|
||||
<ListComponent />
|
||||
</EntityContextProvider>
|
||||
);
|
||||
|
||||
const path = basePath;
|
||||
entityRoutes.push(
|
||||
<Route key={path} path={path} component={Component} />,
|
||||
);
|
||||
}
|
||||
|
||||
if (pages.view) {
|
||||
const ViewComponent = builtComponent(app, pages.view);
|
||||
|
||||
const Component: FC<{}> = () => {
|
||||
const { entityId } = useParams<{ entityId: string }>();
|
||||
return (
|
||||
<EntityContextProvider config={config} id={entityId}>
|
||||
<ViewComponent />
|
||||
</EntityContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const path = `${basePath}/:entityId`;
|
||||
entityRoutes.push(
|
||||
<Route key={path} path={path} component={Component} />,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const pluginRoutes = new Array<JSX.Element>();
|
||||
|
||||
for (const plugin of this.plugins.values()) {
|
||||
const { routes = [] } = plugin[registerSymbol]();
|
||||
pluginRoutes.push(...routes);
|
||||
}
|
||||
|
||||
const routes = [...pluginRoutes, ...entityRoutes];
|
||||
|
||||
return () => (
|
||||
<AppContextProvider app={app}>
|
||||
<Switch>
|
||||
{routes}
|
||||
<Route component={() => <span>404 Not Found</span>} />
|
||||
</Switch>
|
||||
</AppContextProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { createContext, useContext, FC } from 'react';
|
||||
import { App } from './types';
|
||||
|
||||
const Context = createContext<App | undefined>(undefined);
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
};
|
||||
|
||||
export const AppContextProvider: FC<Props> = ({ app, children }) => (
|
||||
<Context.Provider value={app} children={children} />
|
||||
);
|
||||
|
||||
export const useApp = (): App => {
|
||||
const app = useContext(Context);
|
||||
if (!app) {
|
||||
throw new Error('No app context available');
|
||||
}
|
||||
return app;
|
||||
};
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { EntityConfig } from '../entity/EntityKind';
|
||||
|
||||
export type App = {
|
||||
getEntityConfig(kind: string): EntityConfig;
|
||||
};
|
||||
|
||||
export class AppComponentBuilder<T = any> {
|
||||
build(_app: App): ComponentType<T> {
|
||||
throw new Error('Must override build() in AppComponentBuilder');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { ComponentType } from 'react';
|
||||
import { AppComponentBuilder } from '../app/types';
|
||||
|
||||
export type EntityConfig = {
|
||||
kind: string;
|
||||
title: string;
|
||||
icon: React.ComponentType<{ fontSize: number }>;
|
||||
color: {
|
||||
primary: string;
|
||||
secondary: string;
|
||||
};
|
||||
pages: {
|
||||
list?: ComponentType<{}> | AppComponentBuilder;
|
||||
view?: ComponentType<{}> | AppComponentBuilder;
|
||||
};
|
||||
};
|
||||
|
||||
export default class EntityKind {
|
||||
constructor(readonly config: EntityConfig) {}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { createContext, useContext, FC } from 'react';
|
||||
import { EntityConfig } from '../entity/EntityKind';
|
||||
|
||||
type Value = {
|
||||
config: EntityConfig;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
const Context = createContext<Value | undefined>(undefined);
|
||||
|
||||
type Props = {
|
||||
config: EntityConfig;
|
||||
id?: string;
|
||||
};
|
||||
|
||||
export const EntityContextProvider: FC<Props> = ({ config, id, children }) => (
|
||||
<Context.Provider value={{ config, id }} children={children} />
|
||||
);
|
||||
|
||||
export const useEntity = (): { kind: string; id: string } => {
|
||||
const value = useContext(Context);
|
||||
if (!value) {
|
||||
throw new Error('No entity context available');
|
||||
}
|
||||
if (!value.id) {
|
||||
throw new Error('Entity context does not contain entity id');
|
||||
}
|
||||
return { kind: value.config.kind, id: value.id };
|
||||
};
|
||||
|
||||
export const useEntityConfig = (): EntityConfig => {
|
||||
const value = useContext(Context);
|
||||
if (!value) {
|
||||
throw new Error('No entity context available');
|
||||
}
|
||||
return value.config;
|
||||
};
|
||||
|
||||
export const useEntityUri = (): string => {
|
||||
const { kind, id } = useEntity();
|
||||
return `entity:${kind}:${id}`;
|
||||
};
|
||||
@@ -0,0 +1,124 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import { Route, Redirect, Switch } from 'react-router-dom';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import { AppComponentBuilder, App } from '../app/types';
|
||||
import { useEntity, useEntityUri, useEntityConfig } from './EntityContext';
|
||||
import EntityLink from '../../components/EntityLink/EntityLink';
|
||||
|
||||
const EntityLayout: FC<{}> = ({ children }) => {
|
||||
const config = useEntityConfig();
|
||||
return (
|
||||
<div style={{ backgroundColor: config.color.primary }}>{children}</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EntitySidebar: FC<{}> = ({ children }) => {
|
||||
return <List>{children}</List>;
|
||||
};
|
||||
|
||||
const EntitySidebarItem: FC<{ title: string; path: string }> = ({
|
||||
title,
|
||||
path,
|
||||
}) => {
|
||||
const entityUri = useEntityUri();
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
<EntityLink uri={entityUri} subPath={path}>
|
||||
{title}
|
||||
</EntityLink>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
type EntityViewPage = {
|
||||
title: string;
|
||||
path: string;
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
pages: EntityViewPage[];
|
||||
};
|
||||
|
||||
const EntityViewComponent: FC<Props> = ({ pages }) => {
|
||||
const { kind, id } = useEntity();
|
||||
const basePath = `/entity/${kind}/${id}`;
|
||||
|
||||
return (
|
||||
<EntityLayout>
|
||||
<EntitySidebar>
|
||||
{pages.map(({ title, path }) => (
|
||||
<EntitySidebarItem key={path} title={title} path={path} />
|
||||
))}
|
||||
</EntitySidebar>
|
||||
<Switch>
|
||||
{pages.map(({ path, component }) => (
|
||||
<Route
|
||||
key={path}
|
||||
exact
|
||||
path={`${basePath}/${path}`}
|
||||
component={component}
|
||||
/>
|
||||
))}
|
||||
<Redirect from={basePath} to={`${basePath}/${pages[0].path}`} />
|
||||
</Switch>
|
||||
</EntityLayout>
|
||||
);
|
||||
};
|
||||
|
||||
type EntityViewRegistration =
|
||||
| {
|
||||
type: 'page';
|
||||
title: string;
|
||||
path: string;
|
||||
page: AppComponentBuilder;
|
||||
}
|
||||
| {
|
||||
type: 'component';
|
||||
title: string;
|
||||
path: string;
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
export default class EntityViewBuilder extends AppComponentBuilder {
|
||||
private readonly registrations = new Array<EntityViewRegistration>();
|
||||
|
||||
addPage(
|
||||
title: string,
|
||||
path: string,
|
||||
page: AppComponentBuilder,
|
||||
): EntityViewBuilder {
|
||||
this.registrations.push({ type: 'page', title, path, page });
|
||||
return this;
|
||||
}
|
||||
|
||||
addComponent(
|
||||
title: string,
|
||||
path: string,
|
||||
component: ComponentType<any>,
|
||||
): EntityViewBuilder {
|
||||
this.registrations.push({ type: 'component', title, path, component });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(app: App): ComponentType<any> {
|
||||
const pages = this.registrations.map(registration => {
|
||||
switch (registration.type) {
|
||||
case 'page': {
|
||||
const { title, path, page } = registration;
|
||||
return { title, path, component: page.build(app) };
|
||||
}
|
||||
case 'component': {
|
||||
const { title, path, component } = registration;
|
||||
return { title, path, component };
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unknown EntityViewBuilder registration`);
|
||||
}
|
||||
});
|
||||
|
||||
return () => <EntityViewComponent pages={pages} />;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './types';
|
||||
export * from './api';
|
||||
export { useApp } from './app/AppContext';
|
||||
export {
|
||||
useEntity,
|
||||
useEntityConfig,
|
||||
useEntityUri,
|
||||
} from './entityView/EntityContext';
|
||||
@@ -0,0 +1,98 @@
|
||||
import React from 'react';
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
|
||||
export type PluginConfig = {
|
||||
id: string;
|
||||
register?(hooks: PluginHooks): void;
|
||||
};
|
||||
|
||||
export type PluginHooks = {
|
||||
router: Router;
|
||||
};
|
||||
|
||||
export type RouteOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
exact?: boolean;
|
||||
};
|
||||
|
||||
export type RedirectOptions = {
|
||||
// Whether the route path must match exactly, defaults to true.
|
||||
exact?: boolean;
|
||||
};
|
||||
|
||||
export type Router = {
|
||||
registerRoute(
|
||||
path: string,
|
||||
Component: React.ComponentType<any>,
|
||||
options?: RouteOptions,
|
||||
): void;
|
||||
registerRedirect(
|
||||
path: string,
|
||||
target: string,
|
||||
options?: RedirectOptions,
|
||||
): void;
|
||||
};
|
||||
|
||||
export type PluginRegistrationResult = {
|
||||
routes?: JSX.Element[];
|
||||
};
|
||||
|
||||
export const registerSymbol = Symbol('plugin-register');
|
||||
|
||||
export default class Plugin {
|
||||
private result?: PluginRegistrationResult;
|
||||
|
||||
constructor(private readonly config: PluginConfig) {}
|
||||
|
||||
[registerSymbol](): PluginRegistrationResult {
|
||||
if (this.result) {
|
||||
return this.result;
|
||||
}
|
||||
if (!this.config.register) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { id } = this.config;
|
||||
|
||||
const routes = new Array<JSX.Element>();
|
||||
|
||||
this.config.register({
|
||||
router: {
|
||||
registerRoute(path, component, options = {}) {
|
||||
if (path.startsWith('/entity/')) {
|
||||
throw new Error(
|
||||
`Plugin ${id} tried to register forbidden route ${path}`,
|
||||
);
|
||||
}
|
||||
const { exact = true } = options;
|
||||
routes.push(
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
component={component}
|
||||
exact={exact}
|
||||
/>,
|
||||
);
|
||||
},
|
||||
registerRedirect(path, target, options = {}) {
|
||||
if (path.startsWith('/entity/')) {
|
||||
throw new Error(
|
||||
`Plugin ${id} tried to register forbidden redirect ${path}`,
|
||||
);
|
||||
}
|
||||
const { exact = true } = options;
|
||||
routes.push(
|
||||
<Redirect key={path} path={path} to={target} exact={exact} />,
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
this.result = { routes };
|
||||
return this.result;
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `plugin{${this.config.id}}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export type User = {
|
||||
id: string;
|
||||
email: string;
|
||||
};
|
||||
|
||||
export type UserApi = {
|
||||
isLoggedIn(): Promise<boolean>;
|
||||
|
||||
getUser(): Promise<User>;
|
||||
};
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { ComponentType, FC } from 'react';
|
||||
import { App, AppComponentBuilder } from '../app/types';
|
||||
|
||||
type Props = {
|
||||
app: App;
|
||||
cards: ComponentType<any>[];
|
||||
};
|
||||
|
||||
const WidgetViewComponent: FC<Props> = ({ cards }) => {
|
||||
return (
|
||||
<div>
|
||||
{cards.map(CardComponent => (
|
||||
<CardComponent />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type WidgetViewRegistration = {
|
||||
type: 'component';
|
||||
component: ComponentType<any>;
|
||||
};
|
||||
|
||||
export default class WidgetViewBuilder extends AppComponentBuilder {
|
||||
private readonly registrations = new Array<WidgetViewRegistration>();
|
||||
private output?: ComponentType<any>;
|
||||
|
||||
addComponent(component: ComponentType<any>): WidgetViewBuilder {
|
||||
this.registrations.push({ type: 'component', component });
|
||||
return this;
|
||||
}
|
||||
|
||||
build(app: App): ComponentType<any> {
|
||||
if (this.output) {
|
||||
return this.output;
|
||||
}
|
||||
|
||||
const cards = this.registrations.map(reg => {
|
||||
switch (reg.type) {
|
||||
case 'component':
|
||||
return reg.component;
|
||||
default:
|
||||
throw new Error(`Unknown WidgetViewBuilder registration`);
|
||||
}
|
||||
});
|
||||
|
||||
this.output = () => <WidgetViewComponent app={app} cards={cards} />;
|
||||
return this.output;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
type Props = {
|
||||
subPath?: string;
|
||||
} & (
|
||||
| {
|
||||
kind: string;
|
||||
id?: string;
|
||||
}
|
||||
| {
|
||||
uri: string;
|
||||
}
|
||||
);
|
||||
|
||||
function buildPath(kind: string, id?: string, subPath?: string) {
|
||||
if (id) {
|
||||
if (subPath) {
|
||||
return `/entity/${kind}/${id}/${subPath}`;
|
||||
}
|
||||
return `/entity/${kind}/${id}`;
|
||||
}
|
||||
return `/entity/${kind}`;
|
||||
}
|
||||
|
||||
const EntityLink: FC<Props> = ({ subPath, children, ...props }) => {
|
||||
if ('kind' in props) {
|
||||
const { kind, id } = props;
|
||||
return <Link to={buildPath(kind, id, subPath)}>{children}</Link>;
|
||||
} else {
|
||||
const match = props.uri.match(/entity:([^:]+)(:[^:]+)?/);
|
||||
if (!match) {
|
||||
throw new TypeError(`Invalid entity uri: '${props.uri}'`);
|
||||
}
|
||||
|
||||
const [, kind, maybeId] = match;
|
||||
const id = maybeId ? maybeId.slice(1) : undefined;
|
||||
|
||||
return <Link to={buildPath(kind, id, subPath)}>{children}</Link>;
|
||||
}
|
||||
};
|
||||
|
||||
export default EntityLink;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './EntityLink';
|
||||
@@ -0,0 +1,44 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Lifecycle extends Component {
|
||||
static propTypes = {
|
||||
isShorthand: PropTypes.bool,
|
||||
fontSize: PropTypes.string,
|
||||
};
|
||||
}
|
||||
|
||||
const styles = {
|
||||
alpha: {
|
||||
color: '#d00150',
|
||||
fontFamily: 'serif',
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
beta: {
|
||||
color: '#4d65cc',
|
||||
fontFamily: 'serif',
|
||||
fontWeight: 'normal',
|
||||
fontStyle: 'italic',
|
||||
},
|
||||
};
|
||||
|
||||
export class AlphaLabel extends Lifecycle {
|
||||
render() {
|
||||
const style = fontSize => ({ ...styles.alpha, fontSize, ...this.props.style });
|
||||
return this.props.isShorthand ? (
|
||||
<span style={style('120%')}>α</span>
|
||||
) : (
|
||||
<span style={style('100%')}>Alpha</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BetaLabel extends Lifecycle {
|
||||
render() {
|
||||
const fontSize = this.props.fontSize ? this.props.fontSize : this.props.isShorthand ? '120%' : '100%';
|
||||
const style = { ...styles.beta, fontSize, ...this.props.style };
|
||||
|
||||
return this.props.isShorthand ? <span style={style}>β</span> : <span style={style}>Beta</span>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { AlphaLabel, BetaLabel } from './Lifecycle';
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import { pure } from 'recompose';
|
||||
import { withStyles } from '@material-ui/core';
|
||||
|
||||
const styles = theme => ({
|
||||
status: {
|
||||
width: 12,
|
||||
height: 12,
|
||||
display: 'inline-block',
|
||||
marginRight: 1,
|
||||
},
|
||||
ok: {
|
||||
backgroundColor: theme.palette.status.ok,
|
||||
borderRadius: '50%',
|
||||
},
|
||||
warning: {
|
||||
backgroundColor: theme.palette.status.warning,
|
||||
},
|
||||
error: {
|
||||
width: '0',
|
||||
height: '0',
|
||||
borderLeft: '7px solid transparent',
|
||||
borderRight: '7px solid transparent',
|
||||
borderBottom: `14px solid ${theme.palette.status.error}`,
|
||||
},
|
||||
pending: {
|
||||
backgroundColor: theme.palette.status.pending,
|
||||
},
|
||||
failed: {
|
||||
backgroundColor: 'rgba(245, 155, 35, 0.5)',
|
||||
},
|
||||
running: {
|
||||
animation: 'blink 0.8s step-start 0s infinite',
|
||||
backgroundColor: theme.palette.status.running,
|
||||
},
|
||||
'@keyframes blink': {
|
||||
'50%': {
|
||||
backgroundColor: theme.palette.status.background,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const StatusOK = pure(
|
||||
withStyles(styles)(props => (
|
||||
<span className={`${props.classes.status} ${props.classes.ok}`} aria-label="Status OK" {...props} />
|
||||
)),
|
||||
);
|
||||
|
||||
export const StatusWarning = pure(
|
||||
withStyles(styles)(props => (
|
||||
<span className={`${props.classes.status} ${props.classes.warning}`} aria-label="Status warning" {...props} />
|
||||
)),
|
||||
);
|
||||
|
||||
export const StatusError = pure(
|
||||
withStyles(styles)(props => (
|
||||
<span className={`${props.classes.status} ${props.classes.error}`} aria-label="Status error" {...props} />
|
||||
)),
|
||||
);
|
||||
|
||||
export const StatusNA = pure(() => <span aria-label="Status N/A">N/A</span>);
|
||||
|
||||
export const StatusPending = pure(
|
||||
withStyles(styles)(props => (
|
||||
<span className={`${props.classes.status} ${props.classes.pending}`} aria-label="Status pending" {...props} />
|
||||
)),
|
||||
);
|
||||
|
||||
export const StatusRunning = pure(
|
||||
withStyles(styles)(props => (
|
||||
<span className={`${props.classes.status} ${props.classes.running}`} aria-label="Status running" {...props} />
|
||||
)),
|
||||
);
|
||||
|
||||
export const StatusFailed = pure(
|
||||
withStyles(styles)(props => (
|
||||
<span className={`${props.classes.status} ${props.classes.failed}`} aria-label="Status failed" {...props} />
|
||||
)),
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export { StatusError, StatusFailed, StatusNA, StatusOK, StatusPending, StatusRunning, StatusWarning } from './Status';
|
||||
@@ -1,8 +0,0 @@
|
||||
import createPlugin from './createPlugin';
|
||||
|
||||
describe('createPlugin', () => {
|
||||
it('should create a plugin', () => {
|
||||
const plugin = createPlugin({ id: 'my-plugin' });
|
||||
expect(plugin.id).toBe('my-plugin');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +0,0 @@
|
||||
import { BackstagePlugin } from './types';
|
||||
|
||||
export type PluginConfig = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
function createPlugin(config: PluginConfig): BackstagePlugin {
|
||||
return config;
|
||||
}
|
||||
|
||||
export default createPlugin;
|
||||
@@ -1,2 +1,9 @@
|
||||
export * from './types';
|
||||
export { default as createPlugin } from './createPlugin';
|
||||
export * from './api';
|
||||
export { default as EntityLink } from './components/EntityLink';
|
||||
export { default as Page } from '../src/layout/Page';
|
||||
export { gradients, theme } from '../src/layout/Page';
|
||||
export { default as Header } from '../src/layout/Header/Header';
|
||||
export { default as HeaderLabel } from '../src/layout/HeaderLabel';
|
||||
export { default as InfoCard } from '../src/layout/InfoCard';
|
||||
export { default as ErrorBoundary } from '../src/layout/ErrorBoundary';
|
||||
export { default as BackstageTheme } from '../src/theme/BackstageTheme';
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import { Typography, withStyles } from '@material-ui/core';
|
||||
import Helmet from 'react-helmet';
|
||||
import FavoriteButton from 'shared/components/layout/FavoriteButton';
|
||||
|
||||
const styles = theme => ({
|
||||
container: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
marginBottom: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
overflow: 'visible',
|
||||
},
|
||||
rightItemsBox: {
|
||||
flex: '0 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
marginLeft: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
minWidth: 0,
|
||||
overflow: 'visible',
|
||||
},
|
||||
description: {},
|
||||
title: {
|
||||
display: 'inline-flex',
|
||||
},
|
||||
});
|
||||
|
||||
class ContentHeader extends Component {
|
||||
static defaultProps = {
|
||||
favoriteable: true,
|
||||
title: 'Unknown page',
|
||||
};
|
||||
|
||||
render() {
|
||||
const { title, description, favoriteable, children, classes } = this.props;
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet title={title} />
|
||||
<div className={classes.container}>
|
||||
<div className={classes.leftItemsBox}>
|
||||
<Typography variant="h3" className={classes.title} data-testid="header-title">
|
||||
{title}
|
||||
</Typography>
|
||||
{favoriteable && <FavoriteButton />}
|
||||
{description && (
|
||||
<Typography className={classes.description} variant="body2">
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
<div className={classes.rightItemsBox}>{children}</div>
|
||||
</div>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ContentHeader);
|
||||
@@ -0,0 +1,46 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
export default class ErrorBoundary extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
error: null,
|
||||
errorInfo: null,
|
||||
onError: props.onError,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidCatch(error, errorInfo) {
|
||||
console.error(`ErrorBoundary, error: ${error}, info: ${errorInfo}`);
|
||||
this.setState({ error, errorInfo });
|
||||
|
||||
// Exposed for testing
|
||||
if (ErrorBoundary.onError) {
|
||||
ErrorBoundary.onError(error, errorInfo);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { slackChannel } = this.props;
|
||||
const { error, errorInfo } = this.state;
|
||||
|
||||
if (!errorInfo) {
|
||||
return this.props.children;
|
||||
}
|
||||
|
||||
return (
|
||||
<Error error={error} errorInfo={errorInfo} slackChannel={slackChannel} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Importing Error would mean importing a lot of stuff
|
||||
// will take it up in a separate PR
|
||||
const Error = ({ slackChannel }) => {
|
||||
return (
|
||||
<div>
|
||||
Something went wrong here. Please contact {slackChannel} for help.
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './ErrorBoundary';
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
burst: {
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
'z-index': -1,
|
||||
},
|
||||
/* base style of burst shape SVGs */
|
||||
burstShape: {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
height: '100%',
|
||||
'background-repeat': 'no-repeat',
|
||||
'background-size': 'cover',
|
||||
opacity:0.1,
|
||||
}
|
||||
}));
|
||||
|
||||
const Burst = ({theme}) => {
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
<div className={classes.burst} style={{ backgroundImage: theme.gradient }}>
|
||||
<div className={classes.burstShape} style={theme.burstShape} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Burst;
|
||||
@@ -0,0 +1,141 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Typography, withStyles, Tooltip } from '@material-ui/core';
|
||||
import { Theme } from '../Page/Page';
|
||||
// import { Link } from 'shared/components';
|
||||
import Burst from './Burst';
|
||||
import Helmet from 'react-helmet';
|
||||
|
||||
class Header extends Component {
|
||||
static propTypes = {
|
||||
type: PropTypes.string,
|
||||
typeLink: PropTypes.string,
|
||||
title: PropTypes.node.isRequired,
|
||||
tooltip: PropTypes.string,
|
||||
subtitle: PropTypes.node,
|
||||
pageTitleOverride: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
component: PropTypes.object,
|
||||
};
|
||||
|
||||
typeFragment() {
|
||||
const { type, typeLink, classes } = this.props;
|
||||
if (!type) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return typeLink ? (
|
||||
// <Link to={typeLink}>
|
||||
<Typography className={classes.type}>{type}</Typography>
|
||||
// </Link>
|
||||
) : (
|
||||
<Typography className={classes.type}>{type}</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
titleFragment() {
|
||||
const { title, pageTitleOverride, classes, tooltip } = this.props;
|
||||
const FinalTitle = (
|
||||
<Typography className={classes.title} variant="h4">
|
||||
{title || pageTitleOverride}
|
||||
</Typography>
|
||||
);
|
||||
if (tooltip) {
|
||||
return (
|
||||
<Tooltip title={tooltip} placement="top-start">
|
||||
{FinalTitle}
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
return FinalTitle;
|
||||
}
|
||||
|
||||
subtitleFragment() {
|
||||
const { subtitle, classes } = this.props;
|
||||
if (!subtitle) {
|
||||
return null;
|
||||
} else if (typeof subtitle !== 'string') {
|
||||
return subtitle;
|
||||
} else {
|
||||
return (
|
||||
<Typography className={classes.subtitle} variant="subtitle1">
|
||||
{subtitle}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, pageTitleOverride, children, style, classes } = this.props;
|
||||
const pageTitle = pageTitleOverride || title;
|
||||
if (typeof pageTitle !== 'string') {
|
||||
console.warn('<Header/> title prop is not a string, pageTitleOverride should be provided.');
|
||||
}
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<Helmet titleTemplate={`${pageTitle} | %s | Backstage`} defaultTitle={`${pageTitle} | Backstage`} />
|
||||
<Theme.Consumer>
|
||||
{theme => (
|
||||
<header style={style} className={classes.header}>
|
||||
<Burst theme={theme} />
|
||||
<div className={classes.leftItemsBox}>
|
||||
{this.typeFragment()}
|
||||
{this.titleFragment()}
|
||||
{this.subtitleFragment()}
|
||||
</div>
|
||||
<div className={classes.rightItemsBox}>{children}</div>
|
||||
</header>
|
||||
)}
|
||||
</Theme.Consumer>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = theme => ({
|
||||
header: {
|
||||
gridArea: 'pageHeader',
|
||||
padding: theme.spacing(3),
|
||||
minHeight: 118,
|
||||
width: 'calc(100vw - 224px)',
|
||||
boxShadow: '0 0 8px 3px rgba(20, 20, 20, 0.3)',
|
||||
position: 'relative',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'flex-end',
|
||||
alignItems: 'center',
|
||||
},
|
||||
leftItemsBox: {
|
||||
flex: '1 1 auto',
|
||||
},
|
||||
rightItemsBox: {
|
||||
flex: '0 1 auto',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
},
|
||||
title: {
|
||||
color: theme.palette.bursts.fontColor,
|
||||
lineHeight: '1.0em',
|
||||
wordBreak: 'break-all',
|
||||
fontSize: 'calc(24px + 6 * ((100vw - 320px) / 680))',
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
subtitle: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
lineHeight: '1.0em',
|
||||
},
|
||||
type: {
|
||||
textTransform: 'uppercase',
|
||||
fontSize: 9,
|
||||
opacity: 0.8,
|
||||
marginBottom: 10,
|
||||
color: theme.palette.bursts.fontColor,
|
||||
},
|
||||
});
|
||||
|
||||
export default withStyles(styles)(Header);
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import Header from './Header';
|
||||
import { wrapInThemedTestApp } from '../../testUtils';
|
||||
|
||||
jest.mock('react-helmet', () => {
|
||||
return ({ defaultTitle }) => <div>defaultTitle: {defaultTitle}</div>;
|
||||
});
|
||||
|
||||
describe('<Header/>', () => {
|
||||
it('should render with title', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title" />));
|
||||
rendered.getByText('Title');
|
||||
});
|
||||
|
||||
it('should set document title', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title1" />));
|
||||
rendered.getByText('Title1');
|
||||
rendered.getByText('defaultTitle: Title1 | Backstage');
|
||||
});
|
||||
|
||||
it('should override document title', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title1" pageTitleOverride="Title2" />));
|
||||
rendered.getByText('Title1');
|
||||
rendered.getByText('defaultTitle: Title2 | Backstage');
|
||||
});
|
||||
|
||||
it('should have subtitle', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title" subtitle="Subtitle" />));
|
||||
rendered.getByText('Subtitle');
|
||||
});
|
||||
|
||||
it('should have type rendered', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<Header title="Title" type="service" />));
|
||||
rendered.getByText('service');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Header';
|
||||
@@ -0,0 +1,56 @@
|
||||
import React, { Fragment } from 'react';
|
||||
import { IconButton, List, ListItem, ListItemIcon, ListItemText, Popover } from '@material-ui/core';
|
||||
import { default as KebabMenuIcon } from './MenuVertical';
|
||||
|
||||
const ActionItem = ({ label, secondaryLabel, icon, disabled = false, onClick, WrapperComponent = React.Fragment }) => {
|
||||
return (
|
||||
<WrapperComponent>
|
||||
<ListItem
|
||||
data-testid="header-action-item"
|
||||
disabled={disabled}
|
||||
button
|
||||
onClick={event => {
|
||||
if (onClick) {
|
||||
onClick(event);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{icon && <ListItemIcon>{icon}</ListItemIcon>}
|
||||
<ListItemText primary={label} secondary={secondaryLabel} />
|
||||
</ListItem>
|
||||
</WrapperComponent>
|
||||
);
|
||||
};
|
||||
|
||||
const HeaderActionMenu = ({ actionItems }) => {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const anchorElRef = React.useRef(null);
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<IconButton
|
||||
onClick={() => setOpen(true)}
|
||||
data-testid="header-action-menu"
|
||||
ref={anchorElRef}
|
||||
style={{ color: 'white', height: 56, width: 56, marginRight: -4, padding: 0 }}
|
||||
>
|
||||
<KebabMenuIcon titleAccess={'menu'} style={{ fontSize: 40 }} />
|
||||
</IconButton>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorElRef.current}
|
||||
anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }}
|
||||
transformOrigin={{ horizontal: 'right', vertical: 'top' }}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<List>
|
||||
{actionItems.map(actionItem => {
|
||||
return <ActionItem key={actionItem.label} {...actionItem} />;
|
||||
})}
|
||||
</List>
|
||||
</Popover>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
export default HeaderActionMenu;
|
||||
@@ -0,0 +1,70 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent } from '@testing-library/react';
|
||||
import { wrapInThemedTestApp, Keyboard } from '../../testUtils';
|
||||
import HeaderActionMenu from './HeaderActionMenu';
|
||||
|
||||
describe('<ComponentContextMenu />', () => {
|
||||
it('renders without any items and without exploding', () => {
|
||||
render(wrapInThemedTestApp(<HeaderActionMenu actionItems={[]} />));
|
||||
});
|
||||
|
||||
it('can open the menu and click menu items', () => {
|
||||
const onClickFunction = jest.fn();
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label', onClick: onClickFunction }]} />),
|
||||
);
|
||||
expect(rendered.queryByText('Some label')).not.toBeInTheDocument();
|
||||
expect(onClickFunction).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.getByTestId('header-action-menu'));
|
||||
expect(onClickFunction).not.toHaveBeenCalled();
|
||||
expect(rendered.getByTestId('header-action-item')).not.toHaveAttribute('aria-disabled', 'true');
|
||||
fireEvent.click(rendered.queryByText('Some label'));
|
||||
expect(onClickFunction).toHaveBeenCalled();
|
||||
// We do not expect the dropdown to disappear after click
|
||||
expect(rendered.queryByText('Some label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('Disabled', async () => {
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label', disabled: true }]} />),
|
||||
);
|
||||
|
||||
fireEvent.click(rendered.getByTestId('header-action-menu'));
|
||||
expect(rendered.getByTestId('header-action-item')).toHaveAttribute('aria-disabled', 'true');
|
||||
});
|
||||
|
||||
it('Test wrapper, and secondary label', () => {
|
||||
const onClickFunction = jest.fn();
|
||||
const rendered = render(
|
||||
wrapInThemedTestApp(
|
||||
<HeaderActionMenu
|
||||
actionItems={[
|
||||
{
|
||||
label: 'Some label',
|
||||
secondaryLabel: 'Secondary label',
|
||||
WrapperComponent: ({ children }) => <button onClick={onClickFunction}>{children}</button>,
|
||||
},
|
||||
]}
|
||||
/>,
|
||||
),
|
||||
);
|
||||
|
||||
expect(onClickFunction).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.getByTestId('header-action-menu'));
|
||||
expect(onClickFunction).not.toHaveBeenCalled();
|
||||
fireEvent.click(rendered.queryByText('Secondary label'));
|
||||
expect(onClickFunction).toHaveBeenCalled();
|
||||
// We do not expect the dropdown to disappear after click
|
||||
expect(rendered.queryByText('Some label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should close when hitting escape', async () => {
|
||||
const rendered = render(wrapInThemedTestApp(<HeaderActionMenu actionItems={[{ label: 'Some label' }]} />));
|
||||
|
||||
expect(rendered.container.getAttribute('aria-hidden')).toBeNull();
|
||||
fireEvent.click(rendered.getByTestId('header-action-menu'));
|
||||
expect(rendered.container.getAttribute('aria-hidden')).toBe('true');
|
||||
await Keyboard.type(rendered, '<Esc>');
|
||||
expect(rendered.container.getAttribute('aria-hidden')).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import SvgIcon, { SvgIconProps } from '@material-ui/core/SvgIcon';
|
||||
|
||||
const SvgMenuVertical = (props: SvgIconProps) =>
|
||||
React.createElement(
|
||||
SvgIcon,
|
||||
props,
|
||||
<path d="M11 3a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1V4a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2zm0 7a1 1 0 00-1 1v2a1 1 0 001 1h2a1 1 0 001-1v-2a1 1 0 00-1-1h-2z" />,
|
||||
);
|
||||
|
||||
export default SvgMenuVertical;
|
||||
@@ -0,0 +1,50 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Typography, withStyles } from '@material-ui/core';
|
||||
import { Link } from '@material-ui/core';
|
||||
|
||||
const style = theme => ({
|
||||
root: {
|
||||
textAlign: 'left',
|
||||
margin: theme.spacing(2),
|
||||
display: 'inline-block',
|
||||
},
|
||||
label: {
|
||||
color: '#FFFFFF',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: '16px',
|
||||
letterSpacing: 0,
|
||||
fontSize: 14,
|
||||
height: '16px',
|
||||
marginBottom: 2,
|
||||
},
|
||||
value: {
|
||||
color: 'rgba(255, 255, 255, 0.8)',
|
||||
lineHeight: '16px',
|
||||
fontSize: 14,
|
||||
height: '16px',
|
||||
},
|
||||
});
|
||||
|
||||
class HeaderLabel extends Component {
|
||||
static propTypes = {
|
||||
label: PropTypes.string.isRequired,
|
||||
value: PropTypes.node,
|
||||
url: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { label, value, url, classes } = this.props;
|
||||
const content = (
|
||||
<Typography className={classes.value}>{value || '<Unknown>'}</Typography>
|
||||
);
|
||||
return (
|
||||
<span className={classes.root}>
|
||||
<Typography className={classes.label}>{label}</Typography>
|
||||
{url ? <Link href={url}>{content}</Link> : content}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(style)(HeaderLabel);
|
||||
@@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import HeaderLabel from './HeaderLabel';
|
||||
import { wrapInThemedTestApp } from '../../testUtils';
|
||||
|
||||
describe('<HeaderLabel />', () => {
|
||||
it('should have a label', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" />));
|
||||
expect(rendered.getByText('Label')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should say unknown', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" />));
|
||||
expect(rendered.getByText('<Unknown>')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have value', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" />));
|
||||
expect(rendered.getByText('Value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have a link', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<HeaderLabel label="Label" value="Value" url="/test" />));
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
expect(rendered.getByText('Value')).toBeInTheDocument();
|
||||
expect(anchor.href).toBe('http://localhost/test');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tooltip, Link, withStyles } from '@material-ui/core';
|
||||
|
||||
import { StatusError } from '../../components/Status';
|
||||
import HeaderLabel from './HeaderLabel';
|
||||
|
||||
const style = theme => ({
|
||||
notVerified: {
|
||||
color: theme.palette.status.error,
|
||||
borderRadius: 4,
|
||||
padding: '3px 6px',
|
||||
fontSize: '8pt',
|
||||
opacity: 0.8,
|
||||
fontWeight: 'bold',
|
||||
position: 'relative',
|
||||
top: -4,
|
||||
backgroundColor: 'pink',
|
||||
float: 'right',
|
||||
marginLeft: 14,
|
||||
},
|
||||
label: { float: 'left' },
|
||||
});
|
||||
|
||||
class OwnerHeaderLabel extends Component {
|
||||
static propTypes = {
|
||||
owner: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { owner, classes } = this.props;
|
||||
const isBadSquad = owner.type !== 'squad';
|
||||
|
||||
const notVerified = isBadSquad && (
|
||||
<Link href="https://spotify.stackenterprise.co/a/4412/23">
|
||||
<span className={classes.notVerified}>
|
||||
<StatusError style={{ position: 'relative', top: 2 }} /> Squad not verified!
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
const label = (
|
||||
<Tooltip
|
||||
title="This component is not owned by an existing squad. Click the badge to learn how to fix this."
|
||||
placement="bottom"
|
||||
>
|
||||
<span>
|
||||
<span className={classes.label}>{owner.name}</span>
|
||||
</span>
|
||||
</Tooltip>
|
||||
);
|
||||
return (
|
||||
<>
|
||||
<HeaderLabel
|
||||
label="Owner"
|
||||
value={isBadSquad ? label : owner.name}
|
||||
url={owner.name ? `/org/${owner.name}` : ''}
|
||||
/>
|
||||
{notVerified}
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(style)(OwnerHeaderLabel);
|
||||
@@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
import { render, prettyDOM } from '@testing-library/react';
|
||||
import OwnerHeaderLabel from './OwnerHeaderLabel';
|
||||
import { wrapInThemedTestApp } from '../../testUtils';
|
||||
|
||||
const properOwner = { id: 'tools', name: 'tools', type: 'squad' };
|
||||
const badOwner = { id: 'tools-xxx', name: 'tools-xxx' };
|
||||
|
||||
describe('<OwnerHeaderLabel />', () => {
|
||||
it('should have a label', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={properOwner} />));
|
||||
expect(rendered.getByText('Owner')).toBeInTheDocument();
|
||||
expect(rendered.getByText('tools')).toBeInTheDocument();
|
||||
expect(rendered.queryByText('Squad not verified!')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have an org link', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={properOwner} />));
|
||||
const anchor = rendered.container.querySelector('a');
|
||||
expect(anchor.href).toBe('http://localhost/org/tools');
|
||||
});
|
||||
|
||||
it('should have WARNING label', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={badOwner} />));
|
||||
expect(rendered.getByText('Squad not verified!')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should have status error label', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={badOwner} />));
|
||||
expect(rendered.getByLabelText('Status error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should handle empty input', () => {
|
||||
const rendered = render(wrapInThemedTestApp(<OwnerHeaderLabel owner={{}} />));
|
||||
expect(rendered.getByLabelText('Status error')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './HeaderLabel';
|
||||
@@ -0,0 +1,31 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from '@material-ui/core';
|
||||
import { Divider, ListItemText } from '@material-ui/core';
|
||||
import { ListItem, ListItemIcon } from '@material-ui/core';
|
||||
import ArrowIcon from '@material-ui/icons/ArrowForward';
|
||||
|
||||
export default class BottomLink extends Component {
|
||||
static propTypes = {
|
||||
link: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
onClick: PropTypes.func,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { link, title, onClick } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Divider />
|
||||
<Link href={link} onClick={onClick} highlight="none">
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<ArrowIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText>{title}</ListItemText>
|
||||
</ListItem>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '../../testUtils';
|
||||
import BottomLink from './BottomLink';
|
||||
|
||||
const minProps = {
|
||||
title: 'A deepLink title',
|
||||
link: '/mocked',
|
||||
};
|
||||
|
||||
describe('<BottomLink />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const rendered = render(wrapInTestApp(<BottomLink {...minProps} />));
|
||||
expect(rendered.getByText('A deepLink title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,197 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Card,
|
||||
CardActions,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
Divider,
|
||||
withStyles,
|
||||
} from '@material-ui/core';
|
||||
import ErrorBoundary from '../ErrorBoundary/ErrorBoundary';
|
||||
import BottomLink from './BottomLink';
|
||||
|
||||
import textContent from 'react-addons-text-content';
|
||||
|
||||
const BoldHeader = withStyles({ title: { fontWeight: '700' } })(CardHeader);
|
||||
const CardActionsTopRight = withStyles({
|
||||
root: {
|
||||
display: 'inline-block',
|
||||
paddingRight: '16px',
|
||||
paddingTop: '16px',
|
||||
float: 'right',
|
||||
},
|
||||
})(CardActions);
|
||||
|
||||
const VARIANT_STYLES = {
|
||||
card: {
|
||||
flex: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
widget: {
|
||||
height: 430,
|
||||
},
|
||||
fullHeight: {
|
||||
height: '100%',
|
||||
},
|
||||
height100: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(100% - 10px)', // for pages without content header
|
||||
marginBottom: '10px',
|
||||
},
|
||||
contentheader: {
|
||||
height: 'calc(100% - 40px)', // for pages with content header
|
||||
},
|
||||
contentheadertabs: {
|
||||
height: 'calc(100% - 97px)', // for pages with content header and tabs (Tingle)
|
||||
},
|
||||
noShrink: {
|
||||
flexShrink: 0,
|
||||
},
|
||||
minheight300: {
|
||||
minHeight: 300,
|
||||
overflow: 'initial',
|
||||
},
|
||||
},
|
||||
cardContent: {
|
||||
widget: {
|
||||
overflowY: 'auto',
|
||||
height: 332,
|
||||
width: '100%',
|
||||
},
|
||||
fullHeight: {
|
||||
height: 'calc(100% - 50px)',
|
||||
},
|
||||
height100: {
|
||||
height: 'calc(100% - 50px)',
|
||||
},
|
||||
contentRow: {
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* InfoCard is used to display a paper-styled block on the screen, similar to a panel.
|
||||
*
|
||||
* You can custom style an InfoCard with the 'style' (outer container) and 'cardStyle' (inner container)
|
||||
* styles.
|
||||
*
|
||||
* The InfoCard serves as an error boundary. As a result, if you provide a 'slackChannel' property this
|
||||
* specifies the channel to display in the error component that is displayed if an error occurs
|
||||
* in any descendent components.
|
||||
*
|
||||
* By default the InfoCard has no custom layout of its children, but is treated as a block element. A
|
||||
* couple common variants are provided and can be specified via the variant property:
|
||||
*
|
||||
* Display the card full height suitable for DataGrid:
|
||||
*
|
||||
* <InfoCard variant="height100">...</InfoCard>
|
||||
*
|
||||
* Variants can be combined in a whitespace delimited list like so:
|
||||
*
|
||||
* <InfoCard variant="noShrink">...</InfoCard>
|
||||
*/
|
||||
class InfoCard extends Component {
|
||||
static propTypes = {
|
||||
title: PropTypes.oneOfType([PropTypes.element, PropTypes.string]),
|
||||
subheader: PropTypes.oneOfType([PropTypes.node, PropTypes.string]),
|
||||
divider: PropTypes.bool,
|
||||
deepLink: PropTypes.object,
|
||||
slackChannel: PropTypes.string,
|
||||
variant: PropTypes.string,
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
title,
|
||||
subheader,
|
||||
divider,
|
||||
deepLink,
|
||||
children,
|
||||
actions,
|
||||
actionsTopRight,
|
||||
headerStyle,
|
||||
headerProps,
|
||||
classes,
|
||||
slackChannel,
|
||||
variant,
|
||||
} = this.props;
|
||||
|
||||
if (this.props.style) {
|
||||
console.warn(
|
||||
'InfoCard: using `style` property directly, consider migrating your style to variant in InfoCard',
|
||||
);
|
||||
}
|
||||
if (this.props.cardStyle) {
|
||||
console.warn(
|
||||
'InfoCard: using `cardStyle` property directly, consider migrating your style to variant in InfoCard',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* If variant is specified, we build up styles for that particular variant for both
|
||||
* the Card and the CardContent (since these need to be synced)
|
||||
*/
|
||||
let calculatedStyle = {};
|
||||
let calculatedCardStyle = {};
|
||||
|
||||
if (variant) {
|
||||
let variants = variant.split(/[\s]+/g);
|
||||
variants.forEach(name => {
|
||||
calculatedStyle = { ...calculatedStyle, ...VARIANT_STYLES.card[name] };
|
||||
calculatedCardStyle = {
|
||||
...calculatedCardStyle,
|
||||
...VARIANT_STYLES.cardContent[name],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Apply the passed styles on top
|
||||
const computedStyle = { ...calculatedStyle, ...this.props.style };
|
||||
const computedCardStyle = {
|
||||
...calculatedCardStyle,
|
||||
...this.props.cardStyle,
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
style={computedStyle}
|
||||
classes={classes}
|
||||
gacontext={textContent(title)}
|
||||
>
|
||||
<ErrorBoundary slackChannel={slackChannel}>
|
||||
{title && (
|
||||
<BoldHeader
|
||||
title={title}
|
||||
subheader={subheader}
|
||||
style={{ display: 'inline-block', ...headerStyle }}
|
||||
{...headerProps}
|
||||
/>
|
||||
)}
|
||||
{actionsTopRight && (
|
||||
<CardActionsTopRight>{actionsTopRight}</CardActionsTopRight>
|
||||
)}
|
||||
{divider && <Divider />}
|
||||
<CardContent
|
||||
className={this.props.cardClassName}
|
||||
style={computedCardStyle}
|
||||
>
|
||||
{children}
|
||||
</CardContent>
|
||||
{actions && (
|
||||
<CardActions className={this.props.actionsClassName}>
|
||||
{actions}
|
||||
</CardActions>
|
||||
)}
|
||||
{deepLink && <BottomLink {...deepLink} />}
|
||||
</ErrorBoundary>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default InfoCard;
|
||||
@@ -0,0 +1,24 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { wrapInTestApp } from '../../testUtils';
|
||||
import InfoCard from './InfoCard';
|
||||
|
||||
const minProps = {
|
||||
title: 'Some title',
|
||||
deepLink: {
|
||||
title: 'A deepLink title',
|
||||
link: '/mocked',
|
||||
},
|
||||
};
|
||||
|
||||
describe('<InfoCard />', () => {
|
||||
it('renders without exploding', () => {
|
||||
const rendered = render(wrapInTestApp(<InfoCard {...minProps} />));
|
||||
expect(rendered.getByText('Some title')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders a deepLink when prop is set', () => {
|
||||
const rendered = render(wrapInTestApp(<InfoCard {...minProps} />));
|
||||
expect(rendered.getByText('A deepLink title')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './InfoCard';
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { Component } from 'react';
|
||||
import { withStyles } from '@material-ui/core';
|
||||
import { theme } from './PageThemeProvider';
|
||||
|
||||
const styles = theme => ({
|
||||
root: {
|
||||
display: 'grid',
|
||||
gridTemplateAreas:
|
||||
"'pageHeader pageHeader pageHeader' 'pageSubheader pageSubheader pageSubheader' 'pageNav pageContent pageSidebar'",
|
||||
gridTemplateRows: 'auto auto 1fr',
|
||||
gridTemplateColumns: 'auto 1fr auto',
|
||||
minHeight: '100%',
|
||||
paddingBottom: theme.spacing(3),
|
||||
},
|
||||
});
|
||||
|
||||
export const Theme = React.createContext({});
|
||||
|
||||
class Page extends Component {
|
||||
static defaultProps = {
|
||||
theme: theme.other,
|
||||
};
|
||||
|
||||
render() {
|
||||
const { theme, backgroundColor, classes, children, styles = {}, ...otherProps } = this.props;
|
||||
|
||||
return (
|
||||
<Theme.Provider value={theme}>
|
||||
<div style={{ backgroundColor, ...styles }} className={classes.root} {...otherProps}>
|
||||
{children}
|
||||
</div>
|
||||
</Theme.Provider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(Page);
|
||||
@@ -0,0 +1,47 @@
|
||||
export const gradients = {
|
||||
blue: 'linear-gradient(135deg, #2D46B9 0%, #509BF5 100%)',
|
||||
darkBlue: 'linear-gradient(44deg, #1E3264 0%, #A0C3D2 100%)',
|
||||
brown: 'linear-gradient(44deg, #674638 0%, #C39887 100%)',
|
||||
green: 'linear-gradient(-90deg, #1DB954 0%, #006350 100%)',
|
||||
orangeYellow: 'linear-gradient(37deg, #FF6437 0%, #FFC864 100%)',
|
||||
redOrange: 'linear-gradient(37deg, #A72525 0%, #E6542D 100%)',
|
||||
pinkOrange: 'linear-gradient(43deg, #F13DA2 0%, #FF8A48 100%)',
|
||||
purpleBlue: 'linear-gradient(-137deg, #4100F4 0%, #AF2996 100%)',
|
||||
tealGreen: 'linear-gradient(-137deg, #19E68C 0%, #1D7F6E 100%)',
|
||||
violetPeach: 'linear-gradient(44deg, #B39AC8 0%, #FCCBD3 100%)',
|
||||
violetGreen: 'linear-gradient(44deg, #4302F4 0%, #C3EFC8 100%)',
|
||||
purple: 'linear-gradient(-90deg, #a186bd 0%, #7c5c92 100%)',
|
||||
tpm: 'linear-gradient(-137deg, #00FFF2 0%, #035355 100%)',
|
||||
royalBlue: 'linear-gradient(45deg, #000044 0%, #0000DD 61.47%, #0033DD 74%, #4B80D4 100%)',
|
||||
grey: 'linear-gradient(45deg, #111111 0%, #777777 100%)',
|
||||
sunset: 'linear-gradient(148deg, #cf8022 0%, #4e6ec7 100%)',
|
||||
sky: 'linear-gradient(135deg, #69B9FF 0%, #ACCEEC 100%)',
|
||||
};
|
||||
|
||||
export const theme = {
|
||||
service: {
|
||||
activeNavLinkColor: '#1D7F6E',
|
||||
gradient: gradients.tealGreen,
|
||||
burstShape: null,
|
||||
},
|
||||
website: {
|
||||
activeNavLinkColor: '#765d90',
|
||||
gradient: gradients.purple,
|
||||
burstShape: null,
|
||||
},
|
||||
home: {
|
||||
activeNavLinkColor: '#00814e',
|
||||
gradient: gradients.green,
|
||||
burstShape: null,
|
||||
},
|
||||
org: {
|
||||
activeNavLinkColor: '#6044ef',
|
||||
gradient: gradients.violetGreen,
|
||||
burstShape: null,
|
||||
},
|
||||
documentation: {
|
||||
activeNavLinkColor: '#04c2ba',
|
||||
gradient: gradients.tpm,
|
||||
burstShape: null,
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default } from './Page';
|
||||
export { gradients, theme } from './PageThemeProvider';
|
||||
@@ -0,0 +1,188 @@
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
|
||||
const codes = {
|
||||
Tab: 9,
|
||||
Enter: 10,
|
||||
Click: 17 /* This keyboard can click, deal with it */,
|
||||
Esc: 27,
|
||||
};
|
||||
|
||||
export default class Keyboard {
|
||||
static async type(target, input) {
|
||||
await new Keyboard(target).type(input);
|
||||
}
|
||||
|
||||
static async typeDebug(target, input) {
|
||||
await new Keyboard(target, { debug: true }).type(input);
|
||||
}
|
||||
|
||||
static toReadableInput(chars) {
|
||||
return chars.split('').map(char => {
|
||||
switch (char.charCodeAt(0)) {
|
||||
case codes.Tab:
|
||||
return '<Tab>';
|
||||
case codes.Enter:
|
||||
return '<Enter>';
|
||||
case codes.Click:
|
||||
return '<Click>';
|
||||
case codes.Esc:
|
||||
return '<Esc>';
|
||||
default:
|
||||
return char;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
static fromReadableInput(input) {
|
||||
return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => {
|
||||
if (name in codes) {
|
||||
return String.fromCharCode(codes[name]);
|
||||
}
|
||||
throw new Error(`Unknown char name: '${name}'`);
|
||||
});
|
||||
}
|
||||
|
||||
constructor(target, { debug = false } = {}) {
|
||||
this.debug = debug;
|
||||
|
||||
if (target.ownerDocument) {
|
||||
this.document = target.ownerDocument;
|
||||
} else if (target.baseElement) {
|
||||
this.document = target.baseElement.ownerDocument;
|
||||
} else {
|
||||
throw TypeError('Keyboard(target): target must be DOM node or react-testing-library render() output');
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return `Keyboard{document=${this.document}, debug=${this.debug}}`;
|
||||
}
|
||||
|
||||
_log(message, ...args) {
|
||||
if (this.debug) {
|
||||
console.log(`[Keyboard] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
_pretty(element) {
|
||||
const attrs = [...element.attributes].map(attr => `${attr.name}="${attr.value}"`).join(' ');
|
||||
return `<${element.nodeName.toLowerCase()} ${attrs}>`;
|
||||
}
|
||||
|
||||
get focused() {
|
||||
return this.document.activeElement;
|
||||
}
|
||||
|
||||
async type(input) {
|
||||
this._log(`sending sequence '${input}' with initial focus ${this._pretty(this.focused)}`);
|
||||
await this.send(Keyboard.fromReadableInput(input));
|
||||
}
|
||||
|
||||
async send(chars) {
|
||||
for (const key of chars.split('')) {
|
||||
const charCode = key.charCodeAt(0);
|
||||
|
||||
if (charCode === codes.Tab) {
|
||||
await this.tab();
|
||||
continue;
|
||||
}
|
||||
|
||||
const focused = this.focused;
|
||||
if (!focused || focused === this.document.body) {
|
||||
throw Error(`No element focused in document while trying to type '${Keyboard.toReadableInput(chars)}'`);
|
||||
}
|
||||
const nextValue = (focused.value || '') + key;
|
||||
|
||||
if (charCode >= 32) {
|
||||
await this._sendKey(key, charCode, () => {
|
||||
this._log(`sending +${key} = '${nextValue}' to ${this._pretty(focused)}`);
|
||||
fireEvent.change(focused, {
|
||||
target: { value: nextValue },
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
});
|
||||
} else if (charCode === codes.Enter) {
|
||||
await this.enter(focused.value || '');
|
||||
} else if (charCode === codes.Esc) {
|
||||
await this.escape();
|
||||
} else if (charCode === codes.Click) {
|
||||
await this.click();
|
||||
} else {
|
||||
throw new Error(`Unsupported char code, ${charCode}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async click() {
|
||||
this._log(`clicking ${this._pretty(this.focused)}`);
|
||||
await act(async () => fireEvent.click(this.focused));
|
||||
}
|
||||
|
||||
async tab() {
|
||||
await this._sendKey('Tab', codes.Tab, () => {
|
||||
const focusable = this.document.querySelectorAll(
|
||||
[
|
||||
'a[href]',
|
||||
'area[href]',
|
||||
'input:not([disabled])',
|
||||
'select:not([disabled])',
|
||||
'textarea:not([disabled])',
|
||||
'button:not([disabled])',
|
||||
'iframe',
|
||||
'object',
|
||||
'embed',
|
||||
'*[tabindex]',
|
||||
'*[contenteditable]',
|
||||
].join(','),
|
||||
);
|
||||
|
||||
const tabbable = [...focusable].filter(el => {
|
||||
return el.tabIndex >= 0;
|
||||
});
|
||||
|
||||
const focused = this.document.activeElement;
|
||||
const focusedIndex = tabbable.indexOf(focused);
|
||||
const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)];
|
||||
|
||||
this._log(`tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`);
|
||||
nextFocus.focus();
|
||||
});
|
||||
}
|
||||
|
||||
async enter(value) {
|
||||
this._log(`submitting '${value}' via ${this._pretty(this.focused)}`);
|
||||
await act(() =>
|
||||
this._sendKey('Enter', codes.Enter, () => {
|
||||
if (this.focused.type === 'button') {
|
||||
fireEvent.click(this.focused, { target: { value } });
|
||||
} else {
|
||||
fireEvent.submit(this.focused, {
|
||||
target: { value },
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async escape() {
|
||||
this._log(`escape from ${this._pretty(this.focused)}`);
|
||||
await act(async () => this._sendKey('Escape', codes.Esc));
|
||||
}
|
||||
|
||||
async _sendKey(key, charCode, action) {
|
||||
const event = { key, charCode, keyCode: charCode, which: charCode };
|
||||
const focused = this.focused;
|
||||
|
||||
if (fireEvent.keyDown(focused, event)) {
|
||||
if (fireEvent.keyPress(focused, event)) {
|
||||
if (action) {
|
||||
action();
|
||||
}
|
||||
}
|
||||
}
|
||||
fireEvent.keyUp(focused, event);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import Keyboard from './Keyboard';
|
||||
import { render } from '@testing-library/react';
|
||||
|
||||
describe('testUtils.Keyboard', () => {
|
||||
it('types into some inputs with focus and submits a form', async () => {
|
||||
const typed1 = [];
|
||||
const typed2 = [];
|
||||
const typed3 = [];
|
||||
|
||||
let submitted = false;
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
submitted = true;
|
||||
};
|
||||
|
||||
const rendered = render(
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input onChange={({ target: { value } }) => typed1.push(value)} />
|
||||
<input onChange={({ target: { value } }) => typed2.push(value)} autoFocus />
|
||||
<input onChange={({ target: { value } }) => typed3.push(value)} />
|
||||
</form>,
|
||||
);
|
||||
|
||||
const keyboard = new Keyboard(rendered);
|
||||
await keyboard.send('xy');
|
||||
await keyboard.tab();
|
||||
await keyboard.send('abc');
|
||||
await keyboard.enter();
|
||||
|
||||
expect(typed1).toEqual([]);
|
||||
expect(typed2).toEqual(['x', 'xy']);
|
||||
expect(typed3).toEqual(['a', 'ab', 'abc']);
|
||||
expect(submitted).toBe(true);
|
||||
});
|
||||
|
||||
it('can use Keyboard.type to send readable input', async () => {
|
||||
const typed1 = [];
|
||||
const typed2 = [];
|
||||
const typed3 = [];
|
||||
|
||||
let submitted = false;
|
||||
const handleSubmit = event => {
|
||||
event.preventDefault();
|
||||
submitted = true;
|
||||
};
|
||||
|
||||
const rendered = render(
|
||||
<form onSubmit={handleSubmit}>
|
||||
<input defaultValue="1" onChange={({ target: { value } }) => typed1.push(value)} />
|
||||
<input defaultValue="2" onChange={({ target: { value } }) => typed2.push(value)} />
|
||||
<input defaultValue="3" onChange={({ target: { value } }) => typed3.push(value)} />
|
||||
</form>,
|
||||
);
|
||||
|
||||
await Keyboard.type(rendered, '<Tab> a <Tab> b <Tab> c <Enter>');
|
||||
|
||||
expect(typed1).toEqual(['1a']);
|
||||
expect(typed2).toEqual(['2b']);
|
||||
expect(typed3).toEqual(['3c']);
|
||||
expect(submitted).toBe(true);
|
||||
});
|
||||
|
||||
it('should be able to navigate a radio input with click', async () => {
|
||||
const selections = [];
|
||||
|
||||
const rendered = render(
|
||||
<div onChange={({ target: { value } }) => selections.push(value)}>
|
||||
<input type="radio" name="group" value="a" />
|
||||
<input type="radio" name="group" value="b" />
|
||||
<input type="radio" name="group" value="c" />
|
||||
</div>,
|
||||
);
|
||||
|
||||
await Keyboard.type(rendered, '<Tab> <Click> <Tab> <Tab> <Click>');
|
||||
|
||||
expect(selections).toEqual(['a', 'c']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Helpers for testing components
|
||||
*/
|
||||
import React from 'react';
|
||||
import { MuiThemeProvider } from '@material-ui/core';
|
||||
import { ThemeProvider } from '@material-ui/styles';
|
||||
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { Route } from 'react-router-dom';
|
||||
|
||||
import { V1 } from '../theme/BackstageTheme';
|
||||
import ErrorBoundary from '../layout/ErrorBoundary';
|
||||
|
||||
export { default as Keyboard } from './Keyboard';
|
||||
export { default as mockBreakpoint } from './mockBreakpoint';
|
||||
|
||||
export function wrapInTestApp(Component, initialRouterEntries) {
|
||||
const Wrapper = Component instanceof Function ? Component : () => Component;
|
||||
|
||||
return (
|
||||
<MemoryRouter initialEntries={initialRouterEntries || ['/']}>
|
||||
<ErrorBoundary>
|
||||
<Route component={Wrapper} />
|
||||
</ErrorBoundary>
|
||||
</MemoryRouter>
|
||||
);
|
||||
}
|
||||
|
||||
export function wrapInThemedTestApp(component, initialRouterEntries) {
|
||||
const themed = <ThemeProvider theme={V1}>{component}</ThemeProvider>;
|
||||
return wrapInTestApp(themed, initialRouterEntries);
|
||||
}
|
||||
|
||||
export const wrapInTheme = (component, theme = V1) => (
|
||||
<ThemeProvider theme={theme}>{component}</ThemeProvider>
|
||||
);
|
||||
@@ -0,0 +1,73 @@
|
||||
import { act } from '@testing-library/react';
|
||||
|
||||
type Breakpoint = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
||||
|
||||
const queryToBreakpoint = {
|
||||
'(min-width:1920px)': 'xl',
|
||||
'(min-width:1280px)': 'lg',
|
||||
'(min-width:960px)': 'md',
|
||||
'(min-width:600px)': 'sm',
|
||||
'(min-width:0px)': 'xs',
|
||||
} as Record<string, Breakpoint>;
|
||||
|
||||
function toBreakpoint(query: string) {
|
||||
const breakpoint = queryToBreakpoint[query];
|
||||
if (!breakpoint) {
|
||||
throw new Error(`received unknown media query in breakpoint mock: '${query}'`);
|
||||
}
|
||||
return breakpoint;
|
||||
}
|
||||
|
||||
type Listener = (event: { matches: boolean }) => void;
|
||||
|
||||
interface QueryList {
|
||||
addListener(listener: Listener): void;
|
||||
removeListener(listener: Listener): void;
|
||||
matches: boolean;
|
||||
}
|
||||
|
||||
interface Query {
|
||||
query: string;
|
||||
queryList: QueryList;
|
||||
listeners: Set<Listener>;
|
||||
}
|
||||
|
||||
export default function mockBreakpoint(initialBreakpoint: Breakpoint = 'xl') {
|
||||
let currentBreakpoint = initialBreakpoint;
|
||||
const queries = Array<Query>();
|
||||
|
||||
(window as any).matchMedia = (query: string): QueryList => {
|
||||
const listeners = new Set<Listener>();
|
||||
|
||||
const queryList: QueryList = {
|
||||
addListener(listener) {
|
||||
listeners.add(listener);
|
||||
},
|
||||
removeListener(listener) {
|
||||
listeners.delete(listener);
|
||||
},
|
||||
matches: toBreakpoint(query) === currentBreakpoint,
|
||||
};
|
||||
|
||||
queries.push({ query, queryList, listeners });
|
||||
|
||||
return queryList;
|
||||
};
|
||||
|
||||
return {
|
||||
set(breakpoint: Breakpoint) {
|
||||
currentBreakpoint = breakpoint;
|
||||
|
||||
act(() => {
|
||||
queries.forEach(({ query, queryList, listeners }) => {
|
||||
const matches = toBreakpoint(query) === breakpoint;
|
||||
queryList.matches = matches;
|
||||
listeners.forEach(listener => listener({ matches }));
|
||||
});
|
||||
});
|
||||
},
|
||||
remove() {
|
||||
delete window.matchMedia;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
import { createMuiTheme } from '@material-ui/core';
|
||||
import { darken, lighten } from '@material-ui/core/styles/colorManipulator';
|
||||
import { blue, yellow } from '@material-ui/core/colors';
|
||||
|
||||
export const COLORS = {
|
||||
PAGE_BACKGROUND: '#F8F8F8',
|
||||
DEFAULT_PAGE_THEME_COLOR: '#7C3699',
|
||||
DEFAULT_PAGE_THEME_LIGHT_COLOR: '#ECDBF2',
|
||||
ERROR_BACKGROUND_COLOR: '#FFEBEE',
|
||||
ERROR_TEXT_COLOR: '#CA001B',
|
||||
INFO_TEXT_COLOR: '#004e8a',
|
||||
LINK_TEXT: '#0A6EBE',
|
||||
LINK_TEXT_HOVER: '#2196F3',
|
||||
COMPONENT_TYPES: {
|
||||
SERVICE: '#39732E',
|
||||
ENDPOINT: '#B71C1C',
|
||||
PROJECT: '#E657AA',
|
||||
WORKFLOW: '#c15013',
|
||||
},
|
||||
NAMED: {
|
||||
WHITE: '#FEFEFE',
|
||||
},
|
||||
STATUS: {
|
||||
OK: '#1db855',
|
||||
WARNING: '#f49b20',
|
||||
ERROR: '#CA001B',
|
||||
},
|
||||
};
|
||||
|
||||
const extendedThemeConfig = {
|
||||
props: {
|
||||
MuiGrid: {
|
||||
spacing: 2,
|
||||
},
|
||||
MuiSwitch: {
|
||||
color: 'primary',
|
||||
},
|
||||
},
|
||||
palette: {
|
||||
background: {
|
||||
default: COLORS.PAGE_BACKGROUND,
|
||||
informational: '#60a3cb',
|
||||
},
|
||||
status: {
|
||||
ok: COLORS.STATUS.OK,
|
||||
warning: COLORS.STATUS.WARNING,
|
||||
error: COLORS.STATUS.ERROR,
|
||||
running: '#BEBEBE',
|
||||
pending: '#5BC0DE',
|
||||
background: COLORS.NAMED.WHITE,
|
||||
},
|
||||
bursts: {
|
||||
fontColor: COLORS.NAMED.WHITE,
|
||||
slackChannelText: '#ddd',
|
||||
backgroundColor: {
|
||||
service: COLORS.COMPONENT_TYPES.SERVICE,
|
||||
endpoint: COLORS.COMPONENT_TYPES.ENDPOINT,
|
||||
project: COLORS.COMPONENT_TYPES.PROJECT,
|
||||
default: COLORS.DEFAULT_PAGE_THEME_COLOR,
|
||||
},
|
||||
},
|
||||
primary: {
|
||||
main: blue[500],
|
||||
},
|
||||
border: '#E6E6E6',
|
||||
textVerySubtle: '#DDD',
|
||||
textSubtle: '#6E6E6E',
|
||||
highlight: '#FFFBCC',
|
||||
errorBackground: COLORS.ERROR_BACKGROUND_COLOR,
|
||||
warningBackground: '#F59B23',
|
||||
infoBackground: '#ebf5ff',
|
||||
errorText: COLORS.ERROR_TEXT_COLOR,
|
||||
infoText: COLORS.INFO_TEXT_COLOR,
|
||||
warningText: COLORS.NAMED.WHITE,
|
||||
linkHover: COLORS.LINK_TEXT_HOVER,
|
||||
link: COLORS.LINK_TEXT,
|
||||
gold: yellow.A700,
|
||||
},
|
||||
navigation: {
|
||||
width: 220,
|
||||
background: '#333333',
|
||||
},
|
||||
typography: {
|
||||
h4: {
|
||||
// Page name/heading | Dialog titles
|
||||
fontSize: 48,
|
||||
color: '#2D2D2D',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
h3: {
|
||||
// Page titles
|
||||
fontSize: 32,
|
||||
color: '#2D2D2D',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
h2: {
|
||||
// Card titles | Sub headings in dialogs
|
||||
fontSize: 24,
|
||||
color: '#2D2D2D',
|
||||
fontWeight: 'bold',
|
||||
lineHeight: 1.2,
|
||||
marginBottom: 6,
|
||||
},
|
||||
header3: {
|
||||
// Table headers (ALL CAPS!)
|
||||
fontSize: 12,
|
||||
color: '#9E9E9E',
|
||||
},
|
||||
menuItem1: {
|
||||
// Sidebar menu item
|
||||
fontSize: 16,
|
||||
color: '#828282',
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
menuItem2: {
|
||||
// Dropdown menu items | Form labels | Deep links from cards (Go to...)
|
||||
fontSize: 16,
|
||||
color: '#2D2D2D',
|
||||
},
|
||||
text: {
|
||||
// Table entries | Information/error text/messages | General copy/paragraphs
|
||||
fontSize: 13,
|
||||
color: '#2D2D2D',
|
||||
},
|
||||
links: {
|
||||
// Table entries | Information/error text/messages | General copy/paragraphs
|
||||
color: '#509BF5',
|
||||
},
|
||||
tabSelected: {
|
||||
// Selected tab
|
||||
fontSize: 18,
|
||||
color: '#2D2D2D',
|
||||
},
|
||||
tabUnselected: {
|
||||
// Unselected tab
|
||||
fontSize: 18,
|
||||
color: '#9E9E9E',
|
||||
},
|
||||
code: {
|
||||
fontFamily: 'monospace',
|
||||
whiteSpace: 'pre',
|
||||
fontSize: '16px',
|
||||
},
|
||||
caption: {
|
||||
// Restores caption to display: block to match MUI3 behavior; this global override should
|
||||
// be removed if possible.
|
||||
display: 'block',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const createOverrides = theme => {
|
||||
const overrides = {
|
||||
MuiAppBar: {
|
||||
root: {
|
||||
zIndex: 1049,
|
||||
height: 44,
|
||||
},
|
||||
colorPrimary: {
|
||||
backgroundColor: '#212121',
|
||||
},
|
||||
},
|
||||
MuiToolbar: {
|
||||
root: {
|
||||
'@media (min-width:600px)': {
|
||||
minHeight: 44,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableRow: {
|
||||
root: {
|
||||
height: 'auto',
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
},
|
||||
hover: {
|
||||
'&:hover': {
|
||||
cursor: 'pointer',
|
||||
},
|
||||
},
|
||||
head: {
|
||||
height: 'auto',
|
||||
'&:nth-of-type(odd)': {
|
||||
backgroundColor: COLORS.NAMED.WHITE,
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiTableCell: {
|
||||
root: {
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'hidden',
|
||||
verticalAlign: 'middle',
|
||||
lineHeight: '1',
|
||||
margin: 0,
|
||||
padding: '8px',
|
||||
borderBottom: 0,
|
||||
},
|
||||
head: {
|
||||
wordBreak: 'break-word',
|
||||
overflow: 'hidden',
|
||||
color: 'rgb(179, 179, 179)',
|
||||
fontWeight: 'normal',
|
||||
lineHeight: '1',
|
||||
},
|
||||
},
|
||||
MuiTabs: {
|
||||
root: {
|
||||
minHeight: 24,
|
||||
},
|
||||
},
|
||||
MuiTab: {
|
||||
root: {
|
||||
color: theme.palette.link,
|
||||
minHeight: 24,
|
||||
textTransform: 'initial',
|
||||
'&:hover': {
|
||||
color: darken(theme.palette.link, 0.3),
|
||||
background: lighten(theme.palette.link, 0.95),
|
||||
},
|
||||
[theme.breakpoints.up('md')]: {
|
||||
minWidth: 120,
|
||||
fontSize: theme.typography.pxToRem(14),
|
||||
fontWeight: 500,
|
||||
},
|
||||
},
|
||||
textColorPrimary: {
|
||||
color: theme.palette.link,
|
||||
},
|
||||
},
|
||||
MuiTableSortLabel: {
|
||||
root: {
|
||||
color: 'inherit',
|
||||
'&:hover': {
|
||||
color: 'inherit',
|
||||
},
|
||||
'&:focus': {
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
active: {
|
||||
fontWeight: 'bold',
|
||||
color: 'inherit',
|
||||
},
|
||||
},
|
||||
MuiListItemText: {
|
||||
dense: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
text: {
|
||||
padding: '6px 16px',
|
||||
},
|
||||
},
|
||||
MuiChip: {
|
||||
root: {
|
||||
marginRight: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
},
|
||||
MuiDivider: {
|
||||
light: {
|
||||
backgroundColor: '#727272',
|
||||
},
|
||||
},
|
||||
MuiDialogTitle: {
|
||||
root: {
|
||||
minWidth: 600,
|
||||
},
|
||||
},
|
||||
MuiCardHeader: {
|
||||
root: {
|
||||
// Remove bottom padding on titles; there is always a CardContent below that also has padding,
|
||||
// so without this fix there will be too much space below the title.
|
||||
paddingBottom: '0',
|
||||
// Mui 1.2.1 introduced more padding left and right with media queries.
|
||||
// Question is if we should override them or just go with the defaults
|
||||
'@media (min-width:600px)': {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardContent: {
|
||||
root: {
|
||||
'&:last-child': {
|
||||
// There is some odd extra whitespace at the bottom of card content, which makes it look
|
||||
// bottom-heavy and uneven; set it to the same as the other sides
|
||||
paddingBottom: theme.spacing(2),
|
||||
},
|
||||
// Mui 1.2.1 introduced more padding left and right with media queries.
|
||||
// Question is if we should override them or just go with the defaults
|
||||
'@media (min-width:600px)': {
|
||||
paddingLeft: theme.spacing(2),
|
||||
paddingRight: theme.spacing(2),
|
||||
},
|
||||
},
|
||||
},
|
||||
MuiCardActions: {
|
||||
root: {
|
||||
justifyContent: 'flex-end',
|
||||
},
|
||||
},
|
||||
MuiListSubheader: {
|
||||
sticky: {
|
||||
// Sticky subheaders need to be opaque so that they overwrite list items as you scroll down.
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
},
|
||||
},
|
||||
Toolbar: {
|
||||
toolbar: {
|
||||
// Override the toolbar in dx-react-grid table
|
||||
marginTop: '-64px',
|
||||
},
|
||||
},
|
||||
};
|
||||
return {
|
||||
overrides,
|
||||
};
|
||||
};
|
||||
|
||||
const extendedTheme = createMuiTheme(extendedThemeConfig);
|
||||
|
||||
// V1 theming
|
||||
// https://material-ui-next.com/customization/themes/
|
||||
// For CSS it is advised to use JSS, see https://material-ui-next.com/customization/css-in-js/
|
||||
|
||||
const BackstageTheme = { ...extendedTheme, ...createOverrides(extendedTheme) };
|
||||
|
||||
// Temporary workaround for files incorrectly importing the theme directly
|
||||
export const V1 = BackstageTheme;
|
||||
export default BackstageTheme;
|
||||
@@ -1,3 +0,0 @@
|
||||
export type BackstagePlugin = {
|
||||
id: string;
|
||||
};
|
||||
+1
-1
@@ -2,6 +2,6 @@ import plugin from './plugin';
|
||||
|
||||
describe('{{ cookiecutter.plugin_name }}', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin.id).toBe('{{ cookiecutter.plugin_name }}');
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { FC, useState, useEffect } from 'react';
|
||||
import React, { FC, useState, useEffect, Fragment } from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { HelloPromiseClient } from '../../proto/hello_grpc_web_pb';
|
||||
import { HelloRequest } from '../../proto/hello_pb';
|
||||
import { Paper, Typography } from '@material-ui/core';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
||||
const MyComponent: FC<{}> = () => {
|
||||
const [message, setMessage] = useState<string>('');
|
||||
@@ -25,7 +25,7 @@ const MyComponent: FC<{}> = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Paper square>
|
||||
<Fragment>
|
||||
<Typography variant="body1">{message}</Typography>
|
||||
<Typography variant="body1" color="error">
|
||||
{error}
|
||||
@@ -37,7 +37,7 @@ const MyComponent: FC<{}> = () => {
|
||||
>
|
||||
Hello!
|
||||
</Button>
|
||||
</Paper>
|
||||
</Fragment>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -2,6 +2,6 @@ import plugin from './plugin';
|
||||
|
||||
describe('plugin', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin.id).toBe('hello-world');
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Welcome to your home-page plugin!
|
||||
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
...require('@spotify/web-scripts/config/jest.config.js'),
|
||||
setupFilesAfterEnv: ['../jest.setup.ts'],
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
import '@testing-library/jest-dom/extend-expect';
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "@backstage/plugin-home-page",
|
||||
"version": "0.0.0",
|
||||
"main": "src/index.ts",
|
||||
"main:src": "src/index.ts",
|
||||
"devDependencies": {
|
||||
"@backstage/core": "0.0.0",
|
||||
"@spotify/web-scripts": "^6.0.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"react": "^16.12.0",
|
||||
"react-dom": "^16.12.0",
|
||||
"@material-ui/core": "^4.9.1",
|
||||
"@material-ui/icons": "^4.9.1"
|
||||
},
|
||||
"scripts": {
|
||||
"lint": "web-scripts lint",
|
||||
"test": "web-scripts test"
|
||||
},
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import HomePage from './HomePage';
|
||||
|
||||
describe('HomePage', () => {
|
||||
it('should render', () => {
|
||||
const rendered = render(<HomePage />);
|
||||
expect(rendered.baseElement).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { FC } from 'react';
|
||||
import { InfoCard, EntityLink } from '@backstage/core';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Typography } from '@material-ui/core';
|
||||
|
||||
const HomePage: FC<{}> = () => {
|
||||
return (
|
||||
<InfoCard title="Home Page">
|
||||
<Typography variant="body1">Welcome to Backstage!</Typography>
|
||||
<div>
|
||||
<Link to="/login">Go to Login</Link>
|
||||
<EntityLink kind="service" id="backstage-backend">
|
||||
Backstage Backend
|
||||
</EntityLink>
|
||||
<EntityLink uri="entity:service:backstage-lb" subPath="ci-cd">
|
||||
Backstage LB CI/CD
|
||||
</EntityLink>
|
||||
</div>
|
||||
</InfoCard>
|
||||
);
|
||||
};
|
||||
|
||||
export default HomePage;
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './HomePage';
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './plugin';
|
||||
@@ -0,0 +1,7 @@
|
||||
import plugin from './plugin';
|
||||
|
||||
describe('home-page', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import { createPlugin } from '@backstage/core';
|
||||
import HomePage from './components/HomePage';
|
||||
|
||||
export default createPlugin({
|
||||
id: 'home-page',
|
||||
register({ router }) {
|
||||
router.registerRoute('/', HomePage);
|
||||
},
|
||||
});
|
||||
@@ -23,7 +23,7 @@ const LoginComponent: FC<{}> = () => {
|
||||
style={{ marginTop: '24px', marginBottom: '24px' }}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{ textTransform: 'none' }}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,6 @@ import plugin from './plugin';
|
||||
|
||||
describe('login', () => {
|
||||
it('should export plugin', () => {
|
||||
expect(plugin.id).toBe('login');
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
+215
-11
@@ -876,7 +876,7 @@
|
||||
core-js-pure "^3.0.0"
|
||||
regenerator-runtime "^0.13.2"
|
||||
|
||||
"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
|
||||
"@babel/runtime@7.8.4", "@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.3.4", "@babel/runtime@^7.4.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.4", "@babel/runtime@^7.7.6":
|
||||
version "7.8.4"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.8.4.tgz#d79f5a2040f7caa24d53e563aad49cbc05581308"
|
||||
integrity sha512-neAp3zt80trRVBI1x0azq6c57aNBqYZH8KhMm3TaB7wEI5Q4A2SHfBHE8w9gOhI/lrqxtEbXZgQIrHP+wvSGwQ==
|
||||
@@ -2850,6 +2850,11 @@
|
||||
"@types/minimatch" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/history@*":
|
||||
version "4.7.5"
|
||||
resolved "https://registry.npmjs.org/@types/history/-/history-4.7.5.tgz#527d20ef68571a4af02ed74350164e7a67544860"
|
||||
integrity sha512-wLD/Aq2VggCJXSjxEwrMafIP51Z+13H78nXIX0ABEuIGhmB5sNGbR113MOKo+yfw+RDo1ZU3DM6yfnnRF/+ouw==
|
||||
|
||||
"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff"
|
||||
@@ -2932,6 +2937,23 @@
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-router-dom@^5.1.3":
|
||||
version "5.1.3"
|
||||
resolved "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.3.tgz#b5d28e7850bd274d944c0fbbe5d57e6b30d71196"
|
||||
integrity sha512-pCq7AkOvjE65jkGS5fQwQhvUp4+4PVD9g39gXLZViP2UqFiFzsEpB3PKf0O6mdbKsewSK8N14/eegisa/0CwnA==
|
||||
dependencies:
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
"@types/react-router" "*"
|
||||
|
||||
"@types/react-router@*":
|
||||
version "5.1.4"
|
||||
resolved "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.4.tgz#7d70bd905543cb6bcbdcc6bd98902332054f31a6"
|
||||
integrity sha512-PZtnBuyfL07sqCJvGg3z+0+kt6fobc/xmle08jBiezLS8FrmGeiGkJnuxL/8Zgy9L83ypUhniV5atZn/L8n9MQ==
|
||||
dependencies:
|
||||
"@types/history" "*"
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-transition-group@^4.2.0":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.3.tgz#4924133f7268694058e415bf7aea2d4c21131470"
|
||||
@@ -3574,7 +3596,7 @@ arrify@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d"
|
||||
integrity sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=
|
||||
|
||||
asap@^2.0.0, asap@~2.0.6:
|
||||
asap@^2.0.0, asap@~2.0.3, asap@~2.0.6:
|
||||
version "2.0.6"
|
||||
resolved "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46"
|
||||
integrity sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=
|
||||
@@ -4430,6 +4452,11 @@ chalk@^1.0.0, chalk@^1.1.3:
|
||||
strip-ansi "^3.0.0"
|
||||
supports-color "^2.0.0"
|
||||
|
||||
change-emitter@^0.1.2:
|
||||
version "0.1.6"
|
||||
resolved "https://registry.npmjs.org/change-emitter/-/change-emitter-0.1.6.tgz#e8b2fe3d7f1ab7d69a32199aff91ea6931409515"
|
||||
integrity sha1-6LL+PX8at9aaMhma/5HqaTFAlRU=
|
||||
|
||||
chardet@^0.7.0:
|
||||
version "0.7.0"
|
||||
resolved "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e"
|
||||
@@ -5101,6 +5128,11 @@ core-js-pure@^3.0.0:
|
||||
resolved "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz#4bf1ba866e25814f149d4e9aaa08c36173506e3a"
|
||||
integrity sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==
|
||||
|
||||
core-js@^1.0.0:
|
||||
version "1.2.7"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636"
|
||||
integrity sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY=
|
||||
|
||||
core-js@^2.4.0, core-js@^2.5.0:
|
||||
version "2.6.11"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz#38831469f9922bded8ee21c9dc46985e0399308c"
|
||||
@@ -6754,6 +6786,19 @@ fb-watchman@^2.0.0:
|
||||
dependencies:
|
||||
bser "2.1.1"
|
||||
|
||||
fbjs@^0.8.1:
|
||||
version "0.8.17"
|
||||
resolved "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
|
||||
integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
|
||||
dependencies:
|
||||
core-js "^1.0.0"
|
||||
isomorphic-fetch "^2.1.1"
|
||||
loose-envify "^1.0.0"
|
||||
object-assign "^4.1.0"
|
||||
promise "^7.1.1"
|
||||
setimmediate "^1.0.5"
|
||||
ua-parser-js "^0.7.18"
|
||||
|
||||
figgy-pudding@^3.4.1, figgy-pudding@^3.5.1:
|
||||
version "3.5.1"
|
||||
resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790"
|
||||
@@ -7504,6 +7549,11 @@ grpc-web@^1.0.7:
|
||||
resolved "https://registry.npmjs.org/grpc-web/-/grpc-web-1.0.7.tgz#9e4fbcf63d3734515332ab59e42baa7d0d290015"
|
||||
integrity sha512-Fkbz1nyvvt6GC6ODcxh9Fen6LLB3OTCgGHzHwM2Eni44SUhzqPz1UQgFp9sfBEfInOhx3yBdwo9ZLjZAmJ+TtA==
|
||||
|
||||
gud@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.npmjs.org/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
|
||||
integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
|
||||
|
||||
gzip-size@5.1.1:
|
||||
version "5.1.1"
|
||||
resolved "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz#cb9bee692f87c0612b232840a873904e4c135274"
|
||||
@@ -7637,6 +7687,18 @@ hex-color-regex@^1.1.0:
|
||||
resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
|
||||
integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
|
||||
|
||||
history@^4.9.0:
|
||||
version "4.10.1"
|
||||
resolved "https://registry.npmjs.org/history/-/history-4.10.1.tgz#33371a65e3a83b267434e2b3f3b1b4c58aad4cf3"
|
||||
integrity sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
loose-envify "^1.2.0"
|
||||
resolve-pathname "^3.0.0"
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
value-equal "^1.0.1"
|
||||
|
||||
hmac-drbg@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz#d2745701025a6c775a6c545793ed502fc0c649a1"
|
||||
@@ -7646,7 +7708,12 @@ hmac-drbg@^1.0.0:
|
||||
minimalistic-assert "^1.0.0"
|
||||
minimalistic-crypto-utils "^1.0.1"
|
||||
|
||||
hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
|
||||
hoist-non-react-statics@^2.3.1:
|
||||
version "2.5.5"
|
||||
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
|
||||
integrity sha512-rqcy4pJo55FTTLWt+bU8ukscqHeE/e9KWvsOW2b/a3afxQZhwkQdT1rPPCJ0rYXdj4vNcasY8zHTH+jF/qStxw==
|
||||
|
||||
hoist-non-react-statics@^3.1.0, hoist-non-react-statics@^3.2.1, hoist-non-react-statics@^3.3.2:
|
||||
version "3.3.2"
|
||||
resolved "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
|
||||
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
|
||||
@@ -8533,7 +8600,7 @@ is-ssh@^1.3.0:
|
||||
dependencies:
|
||||
protocols "^1.1.0"
|
||||
|
||||
is-stream@^1.0.0, is-stream@^1.1.0:
|
||||
is-stream@^1.0.0, is-stream@^1.0.1, is-stream@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44"
|
||||
integrity sha1-EtSj3U5o4Lec6428hBc66A2RykQ=
|
||||
@@ -8626,6 +8693,14 @@ isobject@^4.0.0:
|
||||
resolved "https://registry.npmjs.org/isobject/-/isobject-4.0.0.tgz#3f1c9155e73b192022a80819bacd0343711697b0"
|
||||
integrity sha512-S/2fF5wH8SJA/kmwr6HYhK/RI/OkhD84k8ntalo0iJjZikgq1XFvR5M8NPT1x5F7fBwCG3qHfnzeP/Vh/ZxCUA==
|
||||
|
||||
isomorphic-fetch@^2.1.1:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9"
|
||||
integrity sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=
|
||||
dependencies:
|
||||
node-fetch "^1.0.1"
|
||||
whatwg-fetch ">=0.10.0"
|
||||
|
||||
isstream@~0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a"
|
||||
@@ -10352,7 +10427,7 @@ longest@^2.0.1:
|
||||
resolved "https://registry.npmjs.org/longest/-/longest-2.0.1.tgz#781e183296aa94f6d4d916dc335d0d17aefa23f8"
|
||||
integrity sha1-eB4YMpaqlPbU2RbcM10NF676I/g=
|
||||
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0:
|
||||
loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3.1, loose-envify@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
|
||||
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
|
||||
@@ -10712,6 +10787,15 @@ min-indent@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/min-indent/-/min-indent-1.0.0.tgz#cfc45c37e9ec0d8f0a0ec3dd4ef7f7c3abe39256"
|
||||
integrity sha1-z8RcN+nsDY8KDsPdTvf3w6vjklY=
|
||||
|
||||
mini-create-react-context@^0.3.0:
|
||||
version "0.3.2"
|
||||
resolved "https://registry.npmjs.org/mini-create-react-context/-/mini-create-react-context-0.3.2.tgz#79fc598f283dd623da8e088b05db8cddab250189"
|
||||
integrity sha512-2v+OeetEyliMt5VHMXsBhABoJ0/M4RCe7fatd/fBy6SMiKazUSEt3gxxypfnk2SHMkdBYvorHRoQxuGoiwbzAw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.4.0"
|
||||
gud "^1.0.0"
|
||||
tiny-warning "^1.0.2"
|
||||
|
||||
mini-css-extract-plugin@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.8.0.tgz#81d41ec4fe58c713a96ad7c723cdb2d0bd4d70e1"
|
||||
@@ -11005,6 +11089,14 @@ node-fetch-npm@^2.0.2:
|
||||
json-parse-better-errors "^1.0.0"
|
||||
safe-buffer "^5.1.1"
|
||||
|
||||
node-fetch@^1.0.1:
|
||||
version "1.7.3"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef"
|
||||
integrity sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==
|
||||
dependencies:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^2.3.0, node-fetch@^2.5.0:
|
||||
version "2.6.0"
|
||||
resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd"
|
||||
@@ -12053,6 +12145,13 @@ path-to-regexp@0.1.7:
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
||||
integrity sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==
|
||||
dependencies:
|
||||
isarray "0.0.1"
|
||||
|
||||
path-type@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz#59c44f7ee491da704da415da5a4070ba4f8fe441"
|
||||
@@ -12960,6 +13059,13 @@ promise-retry@^1.1.1:
|
||||
err-code "^1.0.0"
|
||||
retry "^0.10.0"
|
||||
|
||||
promise@^7.1.1:
|
||||
version "7.3.1"
|
||||
resolved "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf"
|
||||
integrity sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==
|
||||
dependencies:
|
||||
asap "~2.0.3"
|
||||
|
||||
promise@^8.0.3:
|
||||
version "8.0.3"
|
||||
resolved "https://registry.npmjs.org/promise/-/promise-8.0.3.tgz#f592e099c6cddc000d538ee7283bb190452b0bf6"
|
||||
@@ -12982,7 +13088,7 @@ promzard@^0.3.0:
|
||||
dependencies:
|
||||
read "1"
|
||||
|
||||
prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
prop-types@^15.5.4, prop-types@^15.6.2, prop-types@^15.7.2:
|
||||
version "15.7.2"
|
||||
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5"
|
||||
integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==
|
||||
@@ -13192,6 +13298,11 @@ rc@^1.0.1, rc@^1.1.6, rc@^1.2.8:
|
||||
minimist "^1.2.0"
|
||||
strip-json-comments "~2.0.1"
|
||||
|
||||
react-addons-text-content@0.0.4:
|
||||
version "0.0.4"
|
||||
resolved "https://registry.npmjs.org/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5"
|
||||
integrity sha1-0uJZ/clR0diQbAiQIAIQjc6HkuU=
|
||||
|
||||
react-app-polyfill@^1.0.5:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.npmjs.org/react-app-polyfill/-/react-app-polyfill-1.0.6.tgz#890f8d7f2842ce6073f030b117de9130a5f385f0"
|
||||
@@ -13249,11 +13360,67 @@ react-error-overlay@^6.0.5:
|
||||
resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.5.tgz#55d59c2a3810e8b41922e0b4e5f85dcf239bd533"
|
||||
integrity sha512-+DMR2k5c6BqMDSMF8hLH0vYKtKTeikiFW+fj0LClN+XZg4N9b8QUAdHC62CGWNLTi/gnuuemNcNcTFrCvK1f+A==
|
||||
|
||||
react-is@^16.12.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
|
||||
react-fast-compare@^2.0.2:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9"
|
||||
integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==
|
||||
|
||||
react-helmet@5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz#16a7192fdd09951f8e0fe22ffccbf9bb3e591ffa"
|
||||
integrity sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA==
|
||||
dependencies:
|
||||
object-assign "^4.1.1"
|
||||
prop-types "^15.5.4"
|
||||
react-fast-compare "^2.0.2"
|
||||
react-side-effect "^1.1.0"
|
||||
|
||||
react-is@^16.12.0, react-is@^16.6.0, react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4:
|
||||
version "16.12.0"
|
||||
resolved "https://registry.npmjs.org/react-is/-/react-is-16.12.0.tgz#2cc0fe0fba742d97fd527c42a13bec4eeb06241c"
|
||||
integrity sha512-rPCkf/mWBtKc97aLL9/txD8DZdemK0vkA3JMLShjlJB3Pj3s+lpf1KaBzMfQrAmhMQB0n1cU/SUGgKKBCe837Q==
|
||||
|
||||
react-lifecycles-compat@^3.0.2:
|
||||
version "3.0.4"
|
||||
resolved "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
|
||||
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
|
||||
|
||||
react-router-dom@^5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.1.2.tgz#06701b834352f44d37fbb6311f870f84c76b9c18"
|
||||
integrity sha512-7BPHAaIwWpZS074UKaw1FjVdZBSVWEk8IuDXdB+OkLb8vd/WRQIpA4ag9WQk61aEfQs47wHyjWUoUGGZxpQXew==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
history "^4.9.0"
|
||||
loose-envify "^1.3.1"
|
||||
prop-types "^15.6.2"
|
||||
react-router "5.1.2"
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-router@5.1.2:
|
||||
version "5.1.2"
|
||||
resolved "https://registry.npmjs.org/react-router/-/react-router-5.1.2.tgz#6ea51d789cb36a6be1ba5f7c0d48dd9e817d3418"
|
||||
integrity sha512-yjEuMFy1ONK246B+rsa0cUam5OeAQ8pyclRDgpxuSCrAlJ1qN9uZ5IgyKC7gQg0w8OM50NXHEegPh/ks9YuR2A==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.1.2"
|
||||
history "^4.9.0"
|
||||
hoist-non-react-statics "^3.1.0"
|
||||
loose-envify "^1.3.1"
|
||||
mini-create-react-context "^0.3.0"
|
||||
path-to-regexp "^1.7.0"
|
||||
prop-types "^15.6.2"
|
||||
react-is "^16.6.0"
|
||||
tiny-invariant "^1.0.2"
|
||||
tiny-warning "^1.0.0"
|
||||
|
||||
react-side-effect@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz#0e940c78faba0c73b9b0eba9cd3dda8dfb7e7dae"
|
||||
integrity sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w==
|
||||
dependencies:
|
||||
shallowequal "^1.0.1"
|
||||
|
||||
react-transition-group@^4.3.0:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.3.0.tgz#fea832e386cf8796c58b61874a3319704f5ce683"
|
||||
@@ -13472,6 +13639,18 @@ rechoir@^0.6.2:
|
||||
dependencies:
|
||||
resolve "^1.1.6"
|
||||
|
||||
recompose@0.30.0:
|
||||
version "0.30.0"
|
||||
resolved "https://registry.npmjs.org/recompose/-/recompose-0.30.0.tgz#82773641b3927e8c7d24a0d87d65aeeba18aabd0"
|
||||
integrity sha512-ZTrzzUDa9AqUIhRk4KmVFihH0rapdCSMFXjhHbNrjAWxBuUD/guYlyysMnuHjlZC/KRiOKRtB4jf96yYSkKE8w==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.0.0"
|
||||
change-emitter "^0.1.2"
|
||||
fbjs "^0.8.1"
|
||||
hoist-non-react-statics "^2.3.1"
|
||||
react-lifecycles-compat "^3.0.2"
|
||||
symbol-observable "^1.0.4"
|
||||
|
||||
recursive-readdir@2.2.2:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f"
|
||||
@@ -13765,6 +13944,11 @@ resolve-global@1.0.0, resolve-global@^1.0.0:
|
||||
dependencies:
|
||||
global-dirs "^0.1.1"
|
||||
|
||||
resolve-pathname@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz#99d02224d3cf263689becbb393bc560313025dcd"
|
||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||
|
||||
resolve-url-loader@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-3.1.1.tgz#28931895fa1eab9be0647d3b2958c100ae3c0bf0"
|
||||
@@ -14171,7 +14355,7 @@ set-value@^2.0.0, set-value@^2.0.1:
|
||||
is-plain-object "^2.0.3"
|
||||
split-string "^3.0.1"
|
||||
|
||||
setimmediate@^1.0.4:
|
||||
setimmediate@^1.0.4, setimmediate@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
|
||||
integrity sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=
|
||||
@@ -14218,6 +14402,11 @@ shallow-clone@^3.0.0:
|
||||
dependencies:
|
||||
kind-of "^6.0.2"
|
||||
|
||||
shallowequal@^1.0.1:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8"
|
||||
integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==
|
||||
|
||||
shebang-command@^1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
|
||||
@@ -14972,7 +15161,7 @@ svgo@^1.0.0, svgo@^1.2.2:
|
||||
unquote "~1.1.1"
|
||||
util.promisify "~1.0.0"
|
||||
|
||||
symbol-observable@^1.1.0:
|
||||
symbol-observable@^1.0.4, symbol-observable@^1.1.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804"
|
||||
integrity sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==
|
||||
@@ -15184,12 +15373,17 @@ timsort@^0.3.0:
|
||||
resolved "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz#405411a8e7e6339fe64db9a234de11dc31e02bd4"
|
||||
integrity sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=
|
||||
|
||||
tiny-invariant@^1.0.2:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.1.0.tgz#634c5f8efdc27714b7f386c35e6760991d230875"
|
||||
integrity sha512-ytxQvrb1cPc9WBEI/HSeYYoGD0kWnGEOR8RY6KomWLBVhqz0RgTwVO9dLrGz7dC+nN9llyI7OKAgRq8Vq4ZBSw==
|
||||
|
||||
tiny-relative-date@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/tiny-relative-date/-/tiny-relative-date-1.3.0.tgz#fa08aad501ed730f31cc043181d995c39a935e07"
|
||||
integrity sha512-MOQHpzllWxDCHHaDno30hhLfbouoYlOI8YlMNtvKe1zXbjEVhbcEovQxvZrPvtiYW630GQDoMMarCnjfyfHA+A==
|
||||
|
||||
tiny-warning@^1.0.2:
|
||||
tiny-warning@^1.0.0, tiny-warning@^1.0.2:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
|
||||
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
|
||||
@@ -15417,6 +15611,11 @@ typescript@^3.7.4, typescript@^3.7.5:
|
||||
resolved "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz#0692e21f65fd4108b9330238aac11dd2e177a1ae"
|
||||
integrity sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==
|
||||
|
||||
ua-parser-js@^0.7.18:
|
||||
version "0.7.21"
|
||||
resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.21.tgz#853cf9ce93f642f67174273cc34565ae6f308777"
|
||||
integrity sha512-+O8/qh/Qj8CgC6eYBVBykMrNtp5Gebn4dlGD/kKXVkJNDwyrAwSIqwz8CDf+tsAIWVycKcku6gIXJ0qwx/ZXaQ==
|
||||
|
||||
uglify-js@3.4.x:
|
||||
version "3.4.10"
|
||||
resolved "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz#9ad9563d8eb3acdfb8d38597d2af1d815f6a755f"
|
||||
@@ -15715,6 +15914,11 @@ validate-npm-package-name@^3.0.0, validate-npm-package-name@~3.0.0:
|
||||
dependencies:
|
||||
builtins "^1.0.3"
|
||||
|
||||
value-equal@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
@@ -15921,7 +16125,7 @@ whatwg-encoding@^1.0.1, whatwg-encoding@^1.0.3, whatwg-encoding@^1.0.5:
|
||||
dependencies:
|
||||
iconv-lite "0.4.24"
|
||||
|
||||
whatwg-fetch@^3.0.0:
|
||||
whatwg-fetch@>=0.10.0, whatwg-fetch@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz#fc804e458cc460009b1a2b966bc8817d2578aefb"
|
||||
integrity sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q==
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ Collection of all Backstage protobuf definitions.
|
||||
|
||||
Managed with prototool, install instructions at https://github.com/uber/prototool/blob/dev/docs/install.md
|
||||
|
||||
## Install Depenedencies
|
||||
## Install Dependencies
|
||||
|
||||
You will need to have `protoc-gen-go` available in your path. You can find out more information here: https://github.com/golang/protobuf
|
||||
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package spotify.backstage.hello.v1alpha1;
|
||||
|
||||
service Hello {
|
||||
rpc Hello(HelloRequest) returns (HelloReply);
|
||||
}
|
||||
|
||||
message HelloRequest {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
message HelloReply {
|
||||
string message = 1;
|
||||
}
|
||||
Reference in New Issue
Block a user