pull/10616/head
Andrew Watkins 2017-02-22 10:25:05 -06:00
commit 53005e715b
14 changed files with 309 additions and 58 deletions

View File

@ -127,8 +127,8 @@ func (s *Service) RemoveDashboard(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
}
// UpdateDashboard replaces a dashboard
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
// ReplaceDashboard completely replaces a dashboard
func (s *Service) ReplaceDashboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
if err != nil {
@ -165,6 +165,52 @@ func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// UpdateDashboard completely updates either the dashboard name or the cells
func (s *Service) UpdateDashboard(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
idParam, err := strconv.Atoi(httprouter.GetParamFromContext(ctx, "id"))
if err != nil {
msg := fmt.Sprintf("Could not parse dashboard ID: %s", err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
}
id := chronograf.DashboardID(idParam)
orig, err := s.DashboardsStore.Get(ctx, id)
if err != nil {
Error(w, http.StatusNotFound, fmt.Sprintf("ID %d not found", id), s.Logger)
return
}
var req chronograf.Dashboard
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
req.ID = id
if req.Name != "" {
orig.Name = req.Name
} else if len(req.Cells) > 0 {
if err := ValidDashboardRequest(&req); err != nil {
invalidData(w, err, s.Logger)
return
}
orig.Cells = req.Cells
} else {
invalidData(w, fmt.Errorf("Update must include either name or cells"), s.Logger)
return
}
if err := s.DashboardsStore.Update(ctx, orig); err != nil {
msg := fmt.Sprintf("Error updating dashboard ID %d: %v", id, err)
Error(w, http.StatusInternalServerError, msg, s.Logger)
return
}
res := newDashboardResponse(orig)
encodeJSON(w, http.StatusOK, res, s.Logger)
}
// ValidDashboardRequest verifies that the dashboard cells have a query
func ValidDashboardRequest(d *chronograf.Dashboard) error {
if len(d.Cells) == 0 {

View File

@ -117,7 +117,8 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/dashboards/:id", service.DashboardID)
router.DELETE("/chronograf/v1/dashboards/:id", service.RemoveDashboard)
router.PUT("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
router.PUT("/chronograf/v1/dashboards/:id", service.ReplaceDashboard)
router.PATCH("/chronograf/v1/dashboards/:id", service.UpdateDashboard)
/* Authentication */
if opts.UseAuth {

View File

@ -1519,6 +1519,51 @@
}
}
}
},
"patch": {
"tags": [
"layouts"
],
"summary": "Update dashboard information.",
"description": "Update either the dashboard name or the dashboard cells",
"parameters": [
{
"name": "id",
"in": "path",
"type": "integer",
"description": "ID of a dashboard",
"required": true
},
{
"name": "config",
"in": "body",
"description": "dashboard configuration update parameters. Must be either name or cells",
"schema": {
"$ref": "#/definitions/Dashboard"
},
"required": true
}
],
"responses": {
"200": {
"description": "Dashboard has been updated and the new dashboard is returned.",
"schema": {
"$ref": "#/definitions/Dashboard"
}
},
"404": {
"description": "Happens when trying to access a non-existent dashboard.",
"schema": {
"$ref": "#/definitions/Error"
}
},
"default": {
"description": "A processing or an unexpected error.",
"schema": {
"$ref": "#/definitions/Error"
}
}
}
}
}
},

View File

@ -18,7 +18,7 @@ const Dashboard = ({
return (
<div className={classnames({'page-contents': true, 'presentation-mode': inPresentationMode})}>
<div className="container-fluid full-width">
<div className="container-fluid full-width dashboard-view">
{isEditMode ? <Visualizations/> : null}
{Dashboard.renderDashboard(dashboard, timeRange, source, onPositionChange)}
</div>

View File

@ -172,7 +172,7 @@ export const HostPage = React.createClass({
'page-contents': true,
'presentation-mode': inPresentationMode,
})}>
<div className="container-fluid full-width">
<div className="container-fluid full-width dashboard-view">
{ (layouts.length > 0) ? this.renderLayouts(layouts) : '' }
</div>
</div>

View File

@ -90,7 +90,7 @@ export const KubernetesDashboard = React.createClass({
'page-contents': true,
'presentation-mode': inPresentationMode,
})}>
<div className="container-fluid full-width">
<div className="container-fluid full-width dashboard-view">
{layouts.length ? this.renderLayouts(layouts) : emptyState}
</div>
</div>

View File

@ -108,6 +108,7 @@ export default React.createClass({
const legendWidth = legendRect.width;
const legendMaxLeft = graphWidth - (legendWidth / 2);
const trueGraphX = (e.pageX - graphRect.left);
const legendTop = graphRect.height + 0
let legendLeft = trueGraphX;
// Enforcing max & min legend offsets
if (trueGraphX < (legendWidth / 2)) {
@ -117,6 +118,7 @@ export default React.createClass({
}
legendContainerNode.style.left = `${legendLeft}px`;
legendContainerNode.style.top = `${legendTop}px`;
setMarker(points);
},
unhighlightCallback() {

View File

@ -12,7 +12,6 @@ const RefreshingSingleStat = AutoRefresh(SingleStat);
const {
arrayOf,
func,
node,
number,
shape,
string,
@ -106,8 +105,8 @@ export const LayoutRenderer = React.createClass({
if (cell.type === 'single-stat') {
return (
<div key={cell.i}>
<h2 className="hosts-graph-heading">{cell.name}</h2>
<div className="hosts-graph graph-container ">
<h2 className="hosts-graph-heading">{cell.name || `Graph`}</h2>
<div className="hosts-graph graph-container">
<RefreshingSingleStat queries={[qs[0]]} autoRefresh={autoRefreshMs} />
</div>
</div>
@ -115,9 +114,12 @@ export const LayoutRenderer = React.createClass({
}
return (
<Wrapper key={cell.i}>
<RefreshingLineGraph queries={qs} autoRefresh={autoRefreshMs} showSingleStat={cell.type === "line-plus-single-stat"} />
</Wrapper>
<div key={cell.i}>
<h2 className="hosts-graph-heading">{cell.name || `Graph`}</h2>
<div className="hosts-graph graph-container">
<RefreshingLineGraph queries={qs} autoRefresh={autoRefreshMs} showSingleStat={cell.type === "line-plus-single-stat"} />
</div>
</div>
);
});
},
@ -142,9 +144,10 @@ export const LayoutRenderer = React.createClass({
rowHeight={83.5}
margin={[layoutMargin, layoutMargin]}
containerPadding={[0, 0]}
useCSSTransforms={false}
onResize={this.triggerWindowResize}
onLayoutChange={this.handleLayoutChange}
useCSSTransforms={true}
draggableHandle={'.hosts-graph-heading'}
>
{this.generateVisualizations()}
</GridLayout>
@ -160,24 +163,4 @@ export const LayoutRenderer = React.createClass({
},
});
const Wrapper = React.createClass({
propTypes: {
children: node.isRequired,
},
render() {
const that = this;
const newChildren = React.Children.map(this.props.children, (child) => {
return React.cloneElement(child, {
height: that.props.style.height,
})
})
return (
<div {...this.props}>
{newChildren}
</div>
);
}
});
export default LayoutRenderer;

View File

@ -25,7 +25,6 @@ export default React.createClass({
y: arrayOf(number),
y2: arrayOf(number),
}),
height: string,
title: string,
isFetchingInitially: bool,
isRefreshing: bool,
@ -101,22 +100,20 @@ export default React.createClass({
}
return (
<div>
<div className={classNames({"graph--hasYLabel": !!(options.ylabel || options.y2label)})}>
{isRefreshing ? this.renderSpinner() : null}
<Dygraph
containerStyle={{width: '95%', height: this.props.height && parseInt(this.props.height, 10) - 20 + 'px' || '100%'}}
overrideLineColors={overrideLineColors}
isGraphFilled={isGraphFilled}
timeSeries={timeSeries}
labels={labels}
options={options}
dygraphSeries={dygraphSeries}
ranges={ranges || this.getRanges()}
ruleValues={ruleValues}
/>
{showSingleStat ? <div className="graph-single-stat single-stat">{roundedValue}</div> : null}
</div>
<div className={classNames({"graph--hasYLabel": !!(options.ylabel || options.y2label)})}>
{isRefreshing ? this.renderSpinner() : null}
<Dygraph
containerStyle={{width: '100%', height: '100%'}}
overrideLineColors={overrideLineColors}
isGraphFilled={isGraphFilled}
timeSeries={timeSeries}
labels={labels}
options={options}
dygraphSeries={dygraphSeries}
ranges={ranges || this.getRanges()}
ruleValues={ruleValues}
/>
{showSingleStat ? <div className="graph-single-stat single-stat">{roundedValue}</div> : null}
</div>
);
},

View File

@ -45,6 +45,7 @@
@import 'pages/hosts';
@import 'pages/kapacitor';
@import 'pages/data-explorer';
@import 'pages/dashboards';
// TODO
@import 'unsorted';

View File

@ -15,9 +15,8 @@
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='fade-out($g20-white, 0.71)', endColorstr='fade-out($g20-white, 0.71)',GradientType=0 );
}
.container--dygraph-legend {
top: 300px !important;
transform: translate(-50%,-6px);
background-color: $g1-raven;
transform: translateX(-50%);
background-color: $g0-obsidian;
display: block;
position: absolute;
padding: 11px;
@ -119,9 +118,9 @@
.graph--hasYLabel {
.dygraph-axis-label-y {
padding: 0 1px 0 12px !important;
padding: 0 1px 0 10px !important;
}
.dygraph-axis-label-y2 {
padding: 0 12px 0 1px !important;
padding: 0 10px 0 1px !important;
}
}

View File

@ -13,9 +13,31 @@
background: linear-gradient(to right, $startColor 0%,$endColor 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
}
@mixin gradient-diag-up($startColor, $endColor) {
background: $startColor;
background: -moz-linear-gradient(45deg, $startColor 0%, $endColor 100%);
background: -webkit-linear-gradient(45deg, $startColor 0%,$endColor 100%);
background: linear-gradient(45deg, $startColor 0%,$endColor 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
}
@mixin gradient-diag-down($startColor, $endColor) {
background: $startColor;
background: -moz-linear-gradient(135deg, $startColor 0%, $endColor 100%);
background: -webkit-linear-gradient(135deg, $startColor 0%,$endColor 100%);
background: linear-gradient(135deg, $startColor 0%,$endColor 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
}
@mixin gradient-r($startColor, $endColor) {
background: $startColor;
background: -moz-radial-gradient(center, ellipse cover, $startColor 0%, $endColor 100%);
background: -webkit-radial-gradient(center, ellipse cover, $startColor 0%,$endColor 100%);
background: radial-gradient(ellipse at center, $startColor 0%,$endColor 100%);
filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='$startColor', endColorstr='$endColor',GradientType=1 );
}
// Custom Scrollbars (Chrome Only)
$scrollbar-width: 16px;
$scrollbar-offset: 3px;
@mixin custom-scrollbar($trackColor, $handleColor) {
&::-webkit-scrollbar {
width: $scrollbar-width;
@ -30,12 +52,12 @@ $scrollbar-width: 16px;
}
&-track-piece {
background-color: $trackColor;
border: 6px solid $trackColor;
border: $scrollbar-offset solid $trackColor;
border-radius: ($scrollbar-width / 2);
}
&-thumb {
background-color: $handleColor;
border: 6px solid $trackColor;
border: $scrollbar-offset solid $trackColor;
border-radius: ($scrollbar-width / 2);
}
&-corner {

View File

@ -0,0 +1,153 @@
$dashboard-item-heading: 30px;
.dashboard-view {
.react-grid-item {
background-color: $g3-castle;
border-radius: $radius;
border: 2px solid $g3-castle;
transition-property: left, top, border-color, background-color;
&.cssTransforms {
transition-property: transform, border-color, background-color;
}
&.resizing {
background-color: fade-out($g3-castle,0.09);
border-color: $c-pool;
border-image-slice: 3%;
border-image-repeat: initial;
border-image-outset: 0;
border-image-width: 2px;
border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=);
z-index: 3;
& > .react-resizable-handle {
&:before, &:after {
background-color: $c-comet;
}
}
}
&.react-draggable-dragging {
background-color: fade-out($g3-castle,0.09);
border-color: $c-pool;
border-image-slice: 3%;
border-image-repeat: initial;
border-image-outset: 0;
border-image-width: 2px;
border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=);
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
&:hover {
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
& > .hosts-graph-heading,
& > .hosts-graph-heading:hover {
background-color: $g4-onyx;
color: $g18-cloud;
cursor: grabbing;
cursor: -moz-grabbing;
cursor: -webkit-grabbing;
}
}
}
.react-grid-placeholder {
@include gradient-diag-down($c-pool,$c-comet);
border: 0;
opacity: 0.3;
z-index: 2;
}
.graph-empty {
background-color: transparent;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
}
.hosts-graph {
background-color: transparent;
position: absolute;
width: 100%;
height: calc(100% - #{$dashboard-item-heading});
top: $dashboard-item-heading;
left: 0;
padding: 0;
& > div:not(.graph-empty) {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
& > div:not(.graph-panel__refreshing) {
position: absolute;
width: 100%;
height: 100%;
padding: 8px 16px;
}
}
.graph-panel__refreshing {
top: (-$dashboard-item-heading + 5px) !important;
}
}
.hosts-graph-heading {
background-color: transparent;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: $dashboard-item-heading;
padding: 0 16px;
display: flex;
align-items: center;
border-radius: $radius;
transition:
color 0.25s ease,
background-color 0.25s ease;
&:hover {
background-color: $g4-onyx;
color: $g18-cloud;
cursor: move; /* fallback if grab cursor is unsupported */
cursor: grab;
cursor: -moz-grab;
cursor: -webkit-grab;
}
}
}
.react-grid-item > .react-resizable-handle {
background-image: none;
cursor: nwse-resize;
&:before,
&:after {
content: '';
display: block;
position: absolute;
height: 2px;
background-color: $g6-smoke;
transition: background-color 0.25s ease;
top: 50%;
left: 50%;
}
&:before {
width: 20px;
transform: translate(-50%,-50%) rotate(-45deg);
}
&:after {
width: 12px;
transform: translate(-3px,2px) rotate(-45deg);
}
&:hover {
&:before, &:after {
background-color: $c-comet;
}
}
}

View File

@ -25,7 +25,9 @@
padding: 8px 16px;
.single-stat {
font-size: 32px;
font-size: 60px;
font-weight: 300;
color: $c-pool;
display: flex;
justify-content: center;
align-items: center;