Unverified Commit 2f70e55d authored by Daniel's avatar Daniel Committed by GitHub

Add lineage link (#1)

* Added a lineage section in the table details side bar. This link can be enabled and customized by overwriting the default config values.
* Cleaned up some global styles by nesting them inside their component classes.
* Removed deprecated 'nav_link' file.
* Added comments for static configs.
parent 3c96a2ca
from marshmallow import Schema, fields, post_dump
from marshmallow.exceptions import ValidationError
from typing import Dict, List
class Link:
def __init__(self, label: str, href: str, target: str, use_router: bool) -> None:
self.label = label
self.href = href
self.target = target
""" Specify usage of React's built-in router vs a simple <a> anchor tag """
self.use_router = use_router
class LinksSchema(Schema):
label = fields.Str(required=True)
href = fields.Str(required=True)
target = fields.Str(required=False)
use_router = fields.Bool(required=False)
class NavLinks:
def __init__(self, links: List = []) -> None:
self.links = links
class NavLinksSchema(Schema):
links = fields.Nested(LinksSchema, many=True)
@post_dump
def validate_data(self, data: Dict) -> None:
links = data.get('links', [])
for link in links:
if link.get('label') is None:
raise ValidationError('All links must have a label')
if link.get('href') is None:
raise ValidationError('All links must have an href')
...@@ -15,6 +15,13 @@ const configDefault: AppConfig = { ...@@ -15,6 +15,13 @@ const configDefault: AppConfig = {
key: 'default-key', key: 'default-key',
sampleRate: 100, sampleRate: 100,
}, },
lineage: {
enabled: false,
generateUrl: (database: string, cluster: string, schema: string, table: string) => {
return `https://DEFAULT_LINEAGE_URL?schema=${schema}&cluster=${cluster}&db=${database}&table=${table}`;
},
iconPath: 'PATH_TO_ICON'
},
navLinks: [ navLinks: [
{ {
label: "Announcements", label: "Announcements",
......
...@@ -8,31 +8,64 @@ export interface AppConfig { ...@@ -8,31 +8,64 @@ export interface AppConfig {
browse: BrowseConfig; browse: BrowseConfig;
exploreSql: ExploreSqlConfig; exploreSql: ExploreSqlConfig;
google: GoogleAnalyticsConfig; google: GoogleAnalyticsConfig;
lineage: LineageConfig;
navLinks: Array<LinkConfig>; navLinks: Array<LinkConfig>;
} }
export interface AppConfigCustom { export interface AppConfigCustom {
google?: GoogleAnalyticsConfig
browse?: BrowseConfig; browse?: BrowseConfig;
exploreSql?: ExploreSqlConfig; exploreSql?: ExploreSqlConfig;
google?: GoogleAnalyticsConfig
lineage?: LineageConfig;
navLinks?: Array<LinkConfig>; navLinks?: Array<LinkConfig>;
} }
/**
* GoogleAnalyticsConfig - Customize 'gtag' - Google Tag Manager.
*
* Key - The unique analytics key for your site
* Sample Rate - The percentage of users (0 - 100) to track site speed.
*/
interface GoogleAnalyticsConfig { interface GoogleAnalyticsConfig {
key: string; key: string;
sampleRate: number; sampleRate: number;
} }
/**
* BrowseConfig - Customize the 'browse' page.
*
* curatedTags - An array of tags to show in a separate section at the top.
* showAllTags - Shows all tags when true, or only curated tags if false
*/
interface BrowseConfig { interface BrowseConfig {
curatedTags: Array<string>; curatedTags: Array<string>;
showAllTags: boolean; showAllTags: boolean;
} }
/**
* ExploreSqlCongfig - Customize an optional section in the 'table details' page where users can run SQL queries.
*
* enabled - Whether to show or hide this section
* generateUrl - Generates a URL to a third party SQL explorable website.
*/
interface ExploreSqlConfig { interface ExploreSqlConfig {
enabled: boolean; enabled: boolean;
generateUrl: (database: string, cluster: string, schema: string, table: string, partitionKey?: string, partitionValue?: string) => string; generateUrl: (database: string, cluster: string, schema: string, table: string, partitionKey?: string, partitionValue?: string) => string;
} }
/**
* LineageConfig - Customize an optional section in the 'table details' page where users can see a table's lineage.
*
* enabled - Whether to show or hide this section
* generateUrl - Generate a URL to the third party lineage website
* iconPath - Path to an icon image to display next to the lineage URL.
*/
interface LineageConfig {
enabled: boolean;
generateUrl: (database: string, cluster: string, schema: string , table: string) => string;
iconPath: string;
}
interface LinkConfig { interface LinkConfig {
href: string; href: string;
label: string; label: string;
......
@import 'variables'; @import 'variables';
/* Detail List Item */
img {
height: 24px;
min-width: 24px;
border: none;
background-color: $gray-lighter;
}
img.icon-down {
-webkit-mask-image: url('/static/images/icons/Down.svg');
mask-image: url('/static/images/icons/Down.svg');
}
img.icon-up {
-webkit-mask-image: url('/static/images/icons/Up.svg');
mask-image: url('/static/images/icons/Up.svg');
}
.expand-icon {
width: 24px;
margin: 0 4px;
}
.detail-list-item { .detail-list-item {
text-decoration: none; text-decoration: none;
display: flex; display: flex;
...@@ -28,78 +9,103 @@ img.icon-up { ...@@ -28,78 +9,103 @@ img.icon-up {
border-top-color: $gray-lighter !important; border-top-color: $gray-lighter !important;
border-bottom-color: $gray-lighter !important; border-bottom-color: $gray-lighter !important;
background-color: transparent !important; background-color: transparent !important;
}
.detail-list-item .description {
color: $gray-light;
max-width: 100%;
font-size: 14px;
min-width: 0;
padding-right: 32px;
}
.truncated, img {
.truncated .editable-text { height: 24px;
white-space: nowrap; min-width: 24px;
overflow: hidden; border: none;
text-overflow: ellipsis; background-color: $gray-lighter;
} }
img.icon-down {
-webkit-mask-image: url('/static/images/icons/Down.svg');
mask-image: url('/static/images/icons/Down.svg');
}
img.icon-up {
-webkit-mask-image: url('/static/images/icons/Up.svg');
mask-image: url('/static/images/icons/Up.svg');
}
.expand-icon {
width: 24px;
margin: 0 4px;
}
.detail-list-item .column-info { .description {
display: flex; color: $gray-light;
flex-direction: row; max-width: 100%;
margin: -10px -4px 0; font-size: 14px;
padding: 10px 4px 0; min-width: 0;
} padding-right: 32px;
.detail-list-item .column-info.expandable { }
cursor: pointer;
}
.column-info.expandable:hover {
background-image: linear-gradient($gradient-1, $gradient-1, white);
}
.detail-list-item .column-stats {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 10px;
width: 100%;
}
.detail-list-item .title {
display: flex;
flex-direction: row;
}
.detail-list-item .name {
font-size: 16px;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
margin-right: 8px;
}
.detail-list-item .type {
color: $gradient-3;
font-size: 13px;
margin-top: 4px;
font-family: 'Menlo-Bold', Helvetica, Arial, sans-serif;
}
.column-info.expandable:hover .type { .truncated,
color: $gradient-4; .truncated .editable-text {
} white-space: nowrap;
.column-info.expandable:hover .expand-icon { overflow: hidden;
background-color: $gradient-4; text-overflow: ellipsis;
} }
.column-stat { .column-info {
flex: 1; display: flex;
min-width: 120px; flex-direction: row;
margin-top: 8px; margin: -10px -4px 0;
max-width: 20%; padding: 10px 4px 0;
}
.column-stat .title { &.expandable {
color: $gray-light; cursor: pointer;
font-size: 12px;
font-family: $font-family-sans-serif-bold; &:hover {
font-weight: $font-weight-sans-serif-bold; background-image: linear-gradient($gradient-1, $gradient-1, white);
}
.column-stat .content { .type {
color: $gradient-4; color: $gradient-4;
font-size: 14px; }
.expand-icon {
background-color: $gradient-4;
}
}
}
}
.column-stats {
display: flex;
flex-direction: row;
flex-wrap: wrap;
margin-top: 10px;
width: 100%;
}
.title {
display: flex;
flex-direction: row;
}
.name {
font-size: 16px;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
margin-right: 8px;
}
.type {
color: $gradient-3;
font-size: 13px;
margin-top: 4px;
font-family: 'Menlo-Bold', Helvetica, Arial, sans-serif;
}
.column-stat {
flex: 1;
min-width: 120px;
margin-top: 8px;
max-width: 20%;
.title {
color: $gray-light;
font-size: 12px;
font-family: $font-family-sans-serif-bold;
font-weight: $font-weight-sans-serif-bold;
}
.content {
color: $gradient-4;
font-size: 14px;
}
}
} }
...@@ -152,6 +152,16 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps ...@@ -152,6 +152,16 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
} }
} }
getAvatarForLineage = () => {
const href = AppConfig.lineage.generateUrl(this.database, this.cluster, this.schema, this.tableName);
const displayName = `${this.schema}.${this.tableName}`;
return (
<a href={ href } target='_blank'>
<AvatarLabel label={ displayName } src={ AppConfig.lineage.iconPath }/>
</a>
);
};
getExploreSqlUrl = () => { getExploreSqlUrl = () => {
const partition = this.state.tableData.partition; const partition = this.state.tableData.partition;
if (partition.is_partitioned) { if (partition.is_partitioned) {
...@@ -223,6 +233,11 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps ...@@ -223,6 +233,11 @@ class TableDetail extends React.Component<TableDetailProps & RouteComponentProps
entityCardSections.push({'title': 'Source Code', 'contentRenderer': sourceRenderer, 'isEditable': false}); entityCardSections.push({'title': 'Source Code', 'contentRenderer': sourceRenderer, 'isEditable': false});
} }
// "Lineage" Section
if (AppConfig.lineage.enabled) {
entityCardSections.push({'title': 'Table Lineage', 'contentRenderer': this.getAvatarForLineage, 'isEditable': false});
}
// "Preview" Section // "Preview" Section
const previewSectionRenderer = () => { const previewSectionRenderer = () => {
return ( return (
......
...@@ -5,67 +5,58 @@ ...@@ -5,67 +5,58 @@
right: 25px; right: 25px;
bottom: 75px; bottom: 75px;
z-index: 2; z-index: 2;
-webkit-box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, .5); -webkit-box-shadow: 0 0 10px -1px rgba(0, 0, 0, .5);
-moz-box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, .5); -moz-box-shadow: 0 0 10px -1px rgba(0, 0, 0, .5);
box-shadow: 0px 0px 10px -1px rgba(0, 0, 0, .5); box-shadow: 0 0 10px -1px rgba(0, 0, 0, .5);
}
&.expanded {
.feedback-component.expanded { background-color: white;
background-color: white; padding: 24px;
padding: 24px; width: 400px;
width: 400px; height: auto;
height: auto; min-height: 450px;
min-height: 450px; border: 1px solid $gray-lighter;
border: 1px solid $gray-lighter; border-top: 4px solid $brand-primary;
border-top: 4px solid $brand-primary; border-radius: 0 0 5px 5px;
border-radius: 0px 0px 5px 5px; }
}
.feedback-component.collapsed { &.collapsed {
background-color: $brand-primary; background-color: $brand-primary;
border: 2px solid white; border: 2px solid white;
margin: 0px; margin: 0;
height: 48px; height: 48px;
width: 48px; width: 48px;
border-radius: 100px; /* makes it a circle */ border-radius: 100px; /* makes it a circle */
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
cursor:pointer; cursor: pointer;
}
.feedback-component.collapsed:hover { &:hover {
opacity: 0.5; opacity: 0.5;
} }
}
.feedback-component .title {
color: $gray-light; .title {
font-size: 12px; color: $gray-light;
font-family: $font-family-sans-serif-bold; font-size: 12px;
font-weight: $font-weight-sans-serif-bold; font-family: $font-family-sans-serif-bold;
height: 24px; font-weight: $font-weight-sans-serif-bold;
line-height: 32px; height: 24px;
} line-height: 32px;
}
.feedback-component img.icon-speech {
background-color: $brand-primary; img.icon-speech {
height: 36px; background-color: $brand-primary;
width: 24px; height: 36px;
} width: 24px;
}
.feedback-header {
margin-bottom: 8px; .feedback-header {
} margin-bottom: 8px;
}
.feedback-component .btn-group {
margin: 8px auto 16px; .btn-group {
} margin: 8px auto 16px;
}
text {
vertical-align: inherit;
}
img {
background-color: $gray-light;
}
img:hover {
background-color: $gray-lighter;
} }
<html> <html>
<head> <head>
<!-- todo: Move this part to private once we have a way to put private js code -->
<!-- Global site tag (gtag.js) - Google Analytics --> <!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<%= htmlWebpackPlugin.options.config.google.key%>"></script> <script async src="https://www.googletagmanager.com/gtag/js?id=<%= htmlWebpackPlugin.options.config.google.key%>"></script>
<script> <script>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment