Allow setting `start` `end` with relative time (#128)

diff --git a/internal/commands/interceptor/duration.go b/internal/commands/interceptor/duration.go
index 9f8e627..ea9f291 100644
--- a/internal/commands/interceptor/duration.go
+++ b/internal/commands/interceptor/duration.go
@@ -18,6 +18,7 @@
 package interceptor
 
 import (
+	"fmt"
 	"strconv"
 	"time"
 
@@ -28,9 +29,10 @@
 	"github.com/urfave/cli/v2"
 
 	"github.com/apache/skywalking-cli/internal/logger"
+	"github.com/apache/skywalking-cli/internal/model"
 )
 
-func TryParseTime(unparsed string) (api.Step, time.Time, error) {
+func TryParseTime(unparsed string, userStep api.Step) (api.Step, time.Time, error) {
 	var possibleError error
 	for step, layout := range utils.StepFormats {
 		t, err := time.Parse(layout, unparsed)
@@ -39,7 +41,11 @@
 		}
 		possibleError = err
 	}
-	return api.StepSecond, time.Time{}, possibleError
+	duration, err := time.ParseDuration(unparsed)
+	if err == nil {
+		return userStep, time.Now().Add(duration), nil
+	}
+	return userStep, time.Time{}, fmt.Errorf("the given time %v is neither absolute time nor relative time: %+v %+v", unparsed, possibleError, err)
 }
 
 // DurationInterceptor sets the duration if absent, and formats it accordingly,
@@ -47,9 +53,20 @@
 func DurationInterceptor(ctx *cli.Context) error {
 	start := ctx.String("start")
 	end := ctx.String("end")
-	timezone := ctx.String("timezone")
+	userStep := ctx.Generic("step")
+	if timezone := ctx.String("timezone"); timezone != "" {
+		if offset, err := strconv.Atoi(timezone); err == nil {
+			// `offset` is in form of "+1300", while `time.FixedZone` takes offset in seconds
+			time.Local = time.FixedZone("", offset/100*60*60)
+		}
+	}
 
-	startTime, endTime, step, dt := ParseDuration(start, end, timezone)
+	var s api.Step
+	if userStep != nil {
+		s = userStep.(*model.StepEnumValue).Selected
+	}
+
+	startTime, endTime, step, dt := ParseDuration(start, end, s)
 
 	if err := ctx.Set("start", startTime.Format(utils.StepFormats[step])); err != nil {
 		return err
@@ -71,20 +88,11 @@
 //   then: end := now + 30 units, where unit is the precision of `start`, (hours, minutes, etc.)
 // if --start is absent, --end is given,
 //   then: start := end - 30 units, where unit is the precision of `end`, (hours, minutes, etc.)
-func ParseDuration(start, end, timezone string) (startTime, endTime time.Time, step api.Step, dt utils.DurationType) {
-	logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", timezone)
+func ParseDuration(start, end string, userStep api.Step) (startTime, endTime time.Time, step api.Step, dt utils.DurationType) {
+	logger.Log.Debugln("Start time:", start, "end time:", end, "timezone:", time.Local)
 
 	now := time.Now()
 
-	if timezone != "" {
-		if offset, err := strconv.Atoi(timezone); err == nil {
-			// `offset` is in form of "+1300", while `time.FixedZone` takes offset in seconds
-			now = now.In(time.FixedZone("", offset/100*60*60))
-
-			logger.Log.Debugln("Now:", now, "with server timezone:", timezone)
-		}
-	}
-
 	// both are absent
 	if start == "" && end == "" {
 		return now.Add(-30 * time.Minute), now, api.StepMinute, utils.BothAbsent
@@ -94,23 +102,21 @@
 
 	// both are present
 	if len(start) > 0 && len(end) > 0 {
-		start, end = AlignPrecision(start, end)
-
-		if _, startTime, err = TryParseTime(start); err != nil {
+		if userStep, startTime, err = TryParseTime(start, userStep); err != nil {
 			logger.Log.Fatalln("Unsupported time format:", start, err)
 		}
-		if step, endTime, err = TryParseTime(end); err != nil {
+		if step, endTime, err = TryParseTime(end, userStep); err != nil {
 			logger.Log.Fatalln("Unsupported time format:", end, err)
 		}
 
 		return startTime, endTime, step, utils.BothPresent
 	} else if end == "" { // end is absent
-		if step, startTime, err = TryParseTime(start); err != nil {
+		if step, startTime, err = TryParseTime(start, userStep); err != nil {
 			logger.Log.Fatalln("Unsupported time format:", start, err)
 		}
 		return startTime, startTime.Add(30 * utils.StepDuration[step]), step, utils.EndAbsent
 	} else { // start is absent
-		if step, endTime, err = TryParseTime(end); err != nil {
+		if step, endTime, err = TryParseTime(end, userStep); err != nil {
 			logger.Log.Fatalln("Unsupported time format:", end, err)
 		}
 		return endTime.Add(-30 * utils.StepDuration[step]), endTime, step, utils.StartAbsent
diff --git a/internal/flags/duration.go b/internal/flags/duration.go
index f4a3f67..1e3793a 100644
--- a/internal/flags/duration.go
+++ b/internal/flags/duration.go
@@ -25,8 +25,9 @@
 	"github.com/apache/skywalking-cli/internal/model"
 )
 
-var startEndUsage = `"start" and "end" specify a time range during which the query is preformed, 
-		they are both optional and their default values follow the rules below: 
+var startEndUsage = `"start" and "end" specify a time range during which the query is preformed,
+		they can be absolute time like "2019-01-01 12", "2019-01-01 1213", or relative time (to the
+		current time) like "-30m", "30m". They are both optional and their default values follow the rules below: 
 		1. when "start" and "end" are both absent, "start = now - 30 minutes" and "end = now", 
 		namely past 30 minutes; 
 		2. when "start" and "end" are both present, they are aligned to the same precision by 
@@ -39,7 +40,16 @@
 		4. when "start" is present and "end" is absent, will determine the precision of "start" 
 		and then use the precision to calculate "end" (plus 30 units), e.g. "start = 2019-11-09 1204", 
 		the precision is "MINUTE", so "end = start + 30 minutes = 2019-11-09 1234", 
-		and if "start = 2019-11-08 06", the precision is "HOUR", so "end = start + 30HOUR = 2019-11-09 12".`
+		and if "start = 2019-11-08 06", the precision is "HOUR", so "end = start + 30HOUR = 2019-11-09 12".
+		Examples:
+		1. Query the metrics from 20 minutes ago to 10 minutes ago
+		$ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "-20m" --end "-10m"
+		2. Query the metrics from 1 hour ago to 10 minutes ago
+		$ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "-1h" --end "-10m"
+		3. Query the metrics from 1 hour ago to now
+		$ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "-1h" --end "0m"
+		4. Query the metrics from "2021-10-26 1047" to "2021-10-26 1127"
+		$ swctl metrics linear --name=service_resp_time --service-name business-zone::projectB --start "2021-10-26 1047" --end "2021-10-26 1127"`
 
 // DurationFlags are healthcheck flags that involves a duration, composed
 // by a start time, an end time, and a step, which is commonly used
@@ -54,8 +64,8 @@
 		Usage: `end time of the query duration. Check the usage of "start"`,
 	},
 	&cli.GenericFlag{
-		Name:   "step",
-		Hidden: true,
+		Name:  "step",
+		Usage: `time step between start time and end time, should be one of SECOND, MINUTE, HOUR, DAY`,
 		Value: &model.StepEnumValue{
 			Enum:     api.AllStep,
 			Default:  api.StepMinute,
diff --git a/pkg/display/graph/dashboard/global.go b/pkg/display/graph/dashboard/global.go
index 52a01c3..b7f8682 100644
--- a/pkg/display/graph/dashboard/global.go
+++ b/pkg/display/graph/dashboard/global.go
@@ -38,6 +38,7 @@
 	"github.com/mum4k/termdash/terminal/terminalapi"
 	"github.com/urfave/cli/v2"
 
+	"github.com/apache/skywalking-cli/internal/model"
 	"github.com/apache/skywalking-cli/pkg/display/graph/gauge"
 	"github.com/apache/skywalking-cli/pkg/display/graph/heatmap"
 	"github.com/apache/skywalking-cli/pkg/display/graph/linear"
@@ -88,6 +89,7 @@
 var allWidgets *widgets
 
 var initStartStr string
+var initStep = api.StepMinute
 var initEndStr string
 
 var curStartTime time.Time
@@ -131,7 +133,7 @@
 			return nil, err
 		}
 
-		buttons[int(lt)] = b
+		buttons[lt] = b
 	}
 
 	return buttons, nil
@@ -317,11 +319,15 @@
 	initStartStr = ctx.String("start")
 	initEndStr = ctx.String("end")
 
-	_, start, err := interceptor.TryParseTime(initStartStr)
+	if s := ctx.Generic("step"); s != nil {
+		initStep = s.(*model.StepEnumValue).Selected
+	}
+
+	_, start, err := interceptor.TryParseTime(initStartStr, initStep)
 	if err != nil {
 		return
 	}
-	_, end, err := interceptor.TryParseTime(initEndStr)
+	_, end, err := interceptor.TryParseTime(initEndStr, initStep)
 	if err != nil {
 		return
 	}
@@ -355,7 +361,7 @@
 // If the duration doesn't change, an error will be returned, and the dashboard will not refresh.
 // Otherwise, a new duration will be returned, which is used to get the latest global data.
 func updateDuration(interval time.Duration) (api.Duration, error) {
-	step, _, err := interceptor.TryParseTime(initStartStr)
+	step, _, err := interceptor.TryParseTime(initStartStr, initStep)
 	if err != nil {
 		return api.Duration{}, err
 	}